1 Spring的引入
Dao层
public interface UserDao{
void deleteById();
}
public class UserDaoImplForMySQL implements UserDao {
@Override
public void deleteById() {
System.out.println("MySQL数据库正在删除用户信息。。。。。。");
}
}
service层
public interface UserService {
void deleteUser();
}
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImplForMySQL();
@Override
public void deleteUser() {
userDao.deleteById();
}
}
controller层
public class UserAction {
private UserService userService = new UserServiceImpl();
public void deleteRequest(){
userService.deleteUser();
}
}
测试
public class Test {
public static void main(String[] args) {
UserAction userAction = new UserAction();
userAction.deleteRequest();
}
}
缺点
1 违背开闭原则(OCP):对扩展开放,对修改关闭。
如果Dao层,我在引入一个oracle数据库的操作,上面的service层和controller层都需要变化
2 违背依赖倒置原则(DIP):面向接口编程,面向抽象编程,不要面向具体编程
public class UserServiceImpl implements UserService {
// 这里面写了具体实现类,应该写接口,不应该直接写具体实现类
//private UserDao userDao = new UserDaoImplForMySQL();
private UserDao userDao;
@Override
public void deleteUser() {
userDao.deleteById();
}
}
解决
引入控制反转,控制反转主要做下面2件事情
第一件事:我不在程序中采用硬编码的方式来new对象了。(new对象我不管了,new对象的权利交出去了。)
第二件事:我不在程序中采用硬编码的方式来维护对象的关系了。(对象之间关系的维护权,我也不管了,交出去了。)
实现
上面说的控制反转其实是一种概念,具体的实现就是依赖注入,依赖注入又分为2种
第一种:set注入(执行set方法给属性赋值)
第二种:构造方法注入(执行构造方法给属性赋值)
spring框架就实现了控制反转,里面包含依赖注入
2 Spring 入门程序
第一步:添加spring context的依赖,pom.xml配置如下
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>spring6-001-first</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<repositories>
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<!--spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
第二步:定义bean:User
public class User {
}
第三步:编写spring的配置文件:beans.xml。该文件放在类的根路径下。
<?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="userBean" class="com.powernode.spring6.bean.User"/>
</beans>
bean的id和class属性:
● id属性:代表对象的唯一标识。可以看做一个人的身份证号。
● class属性:用来指定要创建的java对象的类名,这个类名必须是全限定类名(带包名)。
第四步:编写测试程序
public class Spring6Test {
@Test
public void testFirst(){
// 初始化Spring容器上下文(解析beans.xml文件,创建所有的bean对象)
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
// 根据id获取bean对象
Object userBean = applicationContext.getBean("userBean");
System.out.println(userBean);
}
}
问题1:bean标签的id属性可以重复吗?
不可以
问题2:底层是怎么创建对象的,是通过反射机制调用无参数构造方法吗?
spring是通过调用类的无参数构造方法来创建对象的,所以要想让spring给你创建对象,必须保证无参数构造方法是存在的。
原理:
// dom4j解析beans.xml文件,从中获取class的全限定类名
// 通过反射机制调用无参数构造方法创建对象
Class clazz = Class.forName("com.powernode.spring6.bean.User");
Object obj = clazz.newInstance();
**问题3:spring 把创建好的对象存储到一个什么样的数据结构当中了呢? **
Map集合key是id,value是bean
问题4:spring配置文件的名字必须叫做beans.xml吗?
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
通过以上的java代码可以看出,这个spring配置文件名字是我们负责提供的,显然spring配置文件的名字是随意的。
问题5:像这样的beans.xml文件可以有多个吗?
spring的配置文件可以有多个,在ClassPathXmlApplicationContext构造方法的参数上传递文件路径即可
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml","spring.xml");
问题6:在配置文件中配置的类必须是自定义的吗,可以使用JDK中的类吗,例如:java.util.Date?
可以的
<bean id="dateBean" class="java.util.Date"/>
问题7:getBean()方法调用时,如果指定的id不存在会怎样?
当id不存在的时候,会出现异常。
问题8:getBean()方法返回的类型是Object,如果访问子类的特有属性和方法时,还需要向下转型,有其它办法可以解决这个问题吗?
User user = applicationContext.getBean("userBean", User.class);
问题9:ClassPathXmlApplicationContext是从类路径中加载配置文件,如果没有在类路径当中,又应该如何加载配置文件呢?
使用FileSystemXmlApplicationContext类进行加载配置文件
ApplicationContext applicationContext2 = new FileSystemXmlApplicationContext("d:/spring6.xml");
问题10:ApplicationContext和BeanFactory的关系。
BeanFactory是Spring容器的超级接口。ApplicationContext是BeanFactory的子接口。
//ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring.xml");
问题11:如何引入log4j2
第一步:引入Log4j2的依赖
<!--log4j2的依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>
第二步:在类的根路径下提供log4j2.xml配置文件(文件名固定为:log4j2.xml,文件必须放到类根路径下。)
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<loggers>
<!--
level指定日志级别,从低到高的优先级:
ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
-->
<root level="DEBUG">
<appender-ref ref="spring6log"/>
</root>
</loggers>
<appenders>
<!--输出日志信息到控制台-->
<console name="spring6log" target="SYSTEM_OUT">
<!--控制日志输出的格式-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
</console>
</appenders>
</configuration>
第三步:使用日志框架
Logger logger = LoggerFactory.getLogger(FirstSpringTest.class);
logger.info("我是一条日志消息");
3 Spring对IOC的实现
3.1 依赖注入
3.1.1 set注入
public class UserDao {
public void insert(){
System.out.println("正在保存用户数据。");
}
}
public class UserService {
private UserDao userDao;
// 使用set方式注入,必须提供set方法。
// 反射机制要调用这个方法给属性赋值的。
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
<?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="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
<bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>
</beans>
我们把属性名称换一下,发现也是ok的
public class UserService {
private UserDao aaa;
// 使用set方式注入,必须提供set方法。
// 反射机制要调用这个方法给属性赋值的。
public void setUserDao(UserDao userDao) {
this.aaa = userDao;
}
public void save(){
aaa.insert();
}
}
说明property标签的name是:setUserDao()方法名演变得到的。
总结:set注入的核心实现原理:通过反射机制调用set方法来给属性赋值,让两个对象之间产生关系
3.1.2 构造方法注入
public class OrderDao {
public void deleteById(){
System.out.println("正在删除订单。。。");
}
}
public class OrderService {
private OrderDao orderDao;
private UserDao userDao;
// 通过反射机制调用构造方法给属性赋值
public OrderService(OrderDao orderDao, UserDao userDao) {
this.orderDao = orderDao;
this.userDao = userDao;
}
public void delete(){
orderDao.deleteById();
userDao.insert();
}
}
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
<!--第一个参数下标是0-->
<constructor-arg index="0" ref="orderDaoBean"/>
<!--第二个参数下标是1-->
<constructor-arg index="1" ref="userDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
不使用参数下标,参数的名字也可以
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
<!--这里使用了构造方法上参数的名字-->
<constructor-arg name="orderDao" ref="orderDaoBean"/>
<constructor-arg name="userDao" ref="userDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
不指定参数下标,不指定参数名字也可以
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
<!--没有指定下标,也没有指定参数名字-->
<constructor-arg ref="orderDaoBean"/>
<constructor-arg ref="userDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
配置文件中构造方法参数的类型顺序和构造方法参数的类型顺序不一致也可以
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
<!--顺序已经和构造方法的参数顺序不同了-->
<constructor-arg ref="userDaoBean"/>
<constructor-arg ref="orderDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
总结
● 可以通过下标
● 可以通过参数名
● 也可以不指定下标和参数名,可以类型自动推断。
3.2 set注入专题
3.2.1 注入外部bean
<?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="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
<bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>
</beans>
外部Bean的特点:bean定义到外面,在property标签中使用ref属性进行注入
3.2.2 注入内部bean
<?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="userServiceBean" class="com.powernode.spring6.service.UserService">
<property name="userDao">
<bean class="com.powernode.spring6.dao.UserDao"/>
</property>
</bean>
</beans>
3.2.3 注入简单类型
<?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="userBean" class="com.powernode.spring6.beans.User">
<!--如果像这种int类型的属性,我们称为简单类型,这种简单类型在注入的时候要使用value属性,不能使用ref-->
<!--<property name="age" value="20"/>-->
<property name="age">
<value>20</value>
</property>
</bean>
</beans>
需要特别注意:如果给简单类型赋值,使用value属性或value标签。而不是ref。
public class A {
private byte b;
private short s;
private int i;
private long l;
private float f;
private double d;
private boolean flag;
private char c;
private Byte b1;
private Short s1;
private Integer i1;
private Long l1;
private Float f1;
private Double d1;
private Boolean flag1;
private Character c1;
private String str;
private Date date;
private Season season;
private URI uri;
private URL url;
private LocalDate localDate;
private Locale locale;
private Class clazz;
// 生成setter方法
// 生成toString方法
}
enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}
<?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="a" class="com.powernode.spring6.beans.A">
<property name="b" value="1"/>
<property name="s" value="1"/>
<property name="i" value="1"/>
<property name="l" value="1"/>
<property name="f" value="1"/>
<property name="d" value="1"/>
<property name="flag" value="false"/>
<property name="c" value="a"/>
<property name="b1" value="2"/>
<property name="s1" value="2"/>
<property name="i1" value="2"/>
<property name="l1" value="2"/>
<property name="f1" value="2"/>
<property name="d1" value="2"/>
<property name="flag1" value="true"/>
<property name="c1" value="a"/>
<property name="str" value="zhangsan"/>
<!--注意:value后面的日期字符串格式不能随便写,必须是Date对象toString()方法执行的结果。-->
<!--如果想使用其他格式的日期字符串,就需要进行特殊处理了。具体怎么处理,可以看后面的课程!!!!-->
<property name="date" value="Fri Sep 30 15:26:38 CST 2022"/>
<property name="season" value="WINTER"/>
<property name="uri" value="/save.do"/>
<!--spring6之后,会自动检查url是否有效,如果无效会报错。-->
<property name="url" value="http://www.baidu.com"/>
<property name="localDate" value="EPOCH"/>
<!--java.util.Locale 主要在软件的本地化时使用。它本身没有什么功能,更多的是作为一个参数辅助其他方法完成输出的本地化。-->
<property name="locale" value="CHINESE"/>
<property name="clazz" value="java.lang.String"/>
</bean>
</beans>
3.2.4 级联属性赋值
<?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="clazzBean" class="com.powernode.spring6.beans.Clazz"/>
<bean id="student" class="com.powernode.spring6.beans.Student">
<property name="name" value="张三"/>
<!--要点1:以下两行配置的顺序不能颠倒-->
<property name="clazz" ref="clazzBean"/>
<!--要点2:clazz属性必须有getter方法-->
<property name="clazz.name" value="高三一班"/>
</bean>
</beans>
3.2.5 注入数组
public class Person {
private String[] favariteFoods;
public void setFavariteFoods(String[] favariteFoods) {
this.favariteFoods = favariteFoods;
}
@Override
public String toString() {
return "Person{" +
"favariteFoods=" + Arrays.toString(favariteFoods) +
'}';
}
}
<?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="person" class="com.powernode.spring6.beans.Person">
<property name="favariteFoods">
<array>
<value>鸡排</value>
<value>汉堡</value>
<value>鹅肝</value>
</array>
</property>
</bean>
</beans>
数组支持对象
<?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="goods1" class="com.powernode.spring6.beans.Goods">
<property name="name" value="西瓜"/>
</bean>
<bean id="goods2" class="com.powernode.spring6.beans.Goods">
<property name="name" value="苹果"/>
</bean>
<bean id="order" class="com.powernode.spring6.beans.Order">
<property name="goods">
<array>
<!--这里使用ref标签即可-->
<ref bean="goods1"/>
<ref bean="goods2"/>
</array>
</property>
</bean>
</beans>
总结:
● 如果数组中是简单类型,使用value标签。
● 如果数组中是非简单类型,使用ref标签
3.2.6 注入List集合
<?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="peopleBean" class="com.powernode.spring6.beans.People">
<property name="names">
<list>
<value>铁锤</value>
<value>张三</value>
<value>张三</value>
<value>张三</value>
<value>狼</value>
</list>
</property>
</bean>
</beans>
3.2.7 注入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/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="peopleBean" class="com.powernode.spring6.beans.People">
<property name="phones">
<set>
<!--非简单类型可以使用ref,简单类型使用value-->
<value>110</value>
<value>110</value>
<value>120</value>
<value>120</value>
<value>119</value>
<value>119</value>
</set>
</property>
</bean>
</beans>
3.2.8 注入Map集合
public class People {
// 一个人有多个住址
private Map<Integer, String> addrs;
public void setAddrs(Map<Integer, String> addrs) {
this.addrs = addrs;
}
//......
@Override
public String toString() {
return "People{" +
"addrs=" + addrs +
", phones=" + phones +
", names=" + names +
'}';
}
}
<?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="peopleBean" class="com.powernode.spring6.beans.People">
<property name="addrs">
<map>
<!--如果key不是简单类型,使用 key-ref 属性-->
<!--如果value不是简单类型,使用 value-ref 属性-->
<entry key="1" value="北京大兴区"/>
<entry key="2" value="上海浦东区"/>
<entry key="3" value="深圳宝安区"/>
</map>
</property>
</bean>
</beans>
要点:
● 如果key是简单类型,使用 key 属性,反之使用 key-ref 属性。
● 如果value是简单类型,使用 value 属性,反之使用 value-ref 属性。
3.2.9 注入Properties
<?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="peopleBean" class="com.powernode.spring6.beans.People">
<property name="properties">
<props>
<prop key="driver">com.mysql.cj.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/spring</prop>
<prop key="username">root</prop>
<prop key="password">123456</prop>
</props>
</property>
</bean>
</beans>
3.2.10 注入null 和空字符串
1 注入空字符串
<?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="vipBean" class="com.powernode.spring6.beans.Vip">
<!--空串的第一种方式-->
<!--<property name="email" value=""/>-->
<!--空串的第二种方式-->
<property name="email">
<value/>
</property>
</bean>
</beans>
2 注入null
<?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="vipBean" class="com.powernode.spring6.beans.Vip" />
<!--第二种方式:使用<null/>-->
<bean id="vipBean" class="com.powernode.spring6.beans.Vip">
<property name="email">
<null/>
</property>
</bean>
</beans>
3.2.11 注入值中含有特殊字符
XML中有5个特殊字符,分别是:<、>、'、"、&
以上5个特殊符号在XML中会被特殊对待,会被当做XML语法的一部分进行解析,如果这些特殊符号直接出现在注入的字符串当中,会报错。
解决方案包括两种:
● 第一种:特殊符号使用转义字符代替。
● 第二种:将含有特殊符号的字符串放到:<![CDATA[]]> 当中。因为放在CDATA区中的数据不会被XML文件解析器解析。
<?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">
<!--方式1:使用转义字符来代替-->
<bean id="mathBean" class="com.powernode.spring6.beans.Math">
<property name="result" value="2 < 3"/>
</bean>
<!--方式2:使用CDATA方式-->
<bean id="mathBean" class="com.powernode.spring6.beans.Math">
<property name="result">
<!--只能使用value标签-->
<value><![CDATA[2 < 3]]></value>
</property>
</bean>
</beans>
3.3 p命名空间注入
本质就是对set注入的简化
使用p命名空间注入的前提条件包括两个:
● 第一:在XML头部信息中添加p命名空间的配置信息:xmlns:p=“http://www.springframework.org/schema/p”
● 第二:p命名空间注入是基于setter方法的,所以需要对应的属性提供setter方法。
public class Customer {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Customer{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
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="customerBean" class="com.powernode.spring6.beans.Customer" p:name="zhangsan" p:age="20"/>
</beans>
3.4 c命名空间注入
本质是对构造方法注入的简化
使用c命名空间的两个前提条件:
第一:需要在xml配置文件头部添加信息:xmlns:c=“http://www.springframework.org/schema/c”
第二:需要提供构造方法。
public class MyTime {
private int year;
private int month;
private int day;
public MyTime(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
@Override
public String toString() {
return "MyTime{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
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="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:year="1970" c:month="1" c:day="1"/>-->
<bean id="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:_0="2008" c:_1="8" c:_2="8"/>
</beans>
3.5 util命名空间注入
本质是可以让配置复用。
<?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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<util:properties id="prop">
<prop key="driver">com.mysql.cj.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/spring</prop>
<prop key="username">root</prop>
<prop key="password">123456</prop>
</util:properties>
<bean id="dataSource1" class="com.powernode.spring6.beans.MyDataSource1">
<property name="properties" ref="prop"/>
</bean>
<bean id="dataSource2" class="com.powernode.spring6.beans.MyDataSource2">
<property name="properties" ref="prop"/>
</bean>
</beans>
3.6 基于XML的自动装配
3.6.1 根据名称的自动装配
public class UserService {
private UserDao aaa;
// 这个set方法非常关键
public void setAaa(UserDao aaa) {
this.aaa = aaa;
}
public void save(){
aaa.insert();
}
}
<?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="userService" class="com.powernode.spring6.service.UserService" autowire="byName"/>
<bean id="aaa" class="com.powernode.spring6.dao.UserDao"/>
</beans>
● UserService Bean中需要添加autowire=“byName”,表示通过名称进行装配。底层会调用set方法进行注入。
● UserService类中有一个UserDao属性,而UserDao属性的名字是aaa,对应的set方法是setAaa(),正好和UserDao Bean的id是一样的。这就是根据名称自动装配。
3.6.2 根据类型的自动装配
<?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">
<!--byType表示根据类型自动装配-->
<bean id="accountService" class="com.powernode.spring6.service.AccountService" autowire="byType"/>
<bean class="com.powernode.spring6.dao.AccountDao"/>
</beans>
1 无论是byName还是byType,在装配的时候都是基于set方法的。所以set方法是必须要提供的。提供构造方法是不行的
2 如果byType,根据类型装配时,如果配置文件中有两个类型一样的bean就会报错
3.7 引入外部配置文件
第一步:写一个数据源类,提供相关属性。
public class MyDataSource implements DataSource {
@Override
public String toString() {
return "MyDataSource{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
private String driver;
private String url;
private String username;
private String password;
public void setDriver(String driver) {
this.driver = driver;
}
public void setUrl(String url) {
this.url = url;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
//......
}
第二步:在类路径下新建jdbc.properties文件,并配置信息。
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring
username=root
password=root123
第三步:在spring配置文件中引入context命名空间。
<?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">
</beans>
第四步:在spring中配置使用jdbc.properties文件。
<?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:property-placeholder location="jdbc.properties"/>
<bean id="dataSource" class="com.powernode.spring6.beans.MyDataSource">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</bean>
</beans>
4 Bean的作用域
4.1 singleton
1 默认情况下,Spring的IoC容器创建的Bean对象是单例的。
2 默认情况下,Bean对象的创建是在初始化Spring上下文的时候就完成的。
4.2 prototype
1 如果想多例,需要加入prototype的scope属性
2 每一次执行getBean()方法的时候创建Bean对象
<?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="sb" class="com.powernode.spring6.beans.SpringBean" scope="singleton" />
-->
<bean id="sb" class="com.powernode.spring6.beans.SpringBean" scope="prototype" />
</beans>
4.3 其他scope
scope属性的值不止两个,它一共包括8个选项:
● singleton:默认的,单例。
● prototype:原型。每调用一次getBean()方法则获取一个新的Bean对象。或每次注入的时候都是新对象。
● request:一个请求对应一个Bean。仅限于在WEB应用中使用。
● session:一个会话对应一个Bean。仅限于在WEB应用中使用。
● global session:portlet应用中专用的。如果在Servlet的WEB应用中使用global session的话,和session一个效果。(portlet和servlet都是规范。servlet运行在servlet容器中,例如Tomcat。portlet运行在portlet容器中。)
● application:一个应用对应一个Bean。仅限于在WEB应用中使用。
● websocket:一个websocket生命周期对应一个Bean。仅限于在WEB应用中使用。
● 自定义scope:很少使用。
5 bean的实例化方式
5.1 通过构造方法实例化
默认情况下,会调用Bean的无参数构造方法。
<?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="userBean" class="com.powernode.spring6.bean.User"/>
</beans>
5.2 通过简单工厂模式实例化
第一步:定义一个Bean
public class Vip {
}
第二步:编写简单工厂模式当中的工厂类
public class VipFactory {
public static Vip get(){
return new Vip();
}
}
第三步:在Spring配置文件中指定创建该Bean的方法(使用factory-method属性指定)
<bean id="vipBean" class="com.powernode.spring6.bean.VipFactory" factory-method="get"/>
5.3 通过factory-bean实例化
第一步:定义一个Bean
public class Order {
}
第二步:定义具体工厂类,工厂类中定义实例方法
public class OrderFactory {
public Order get(){
return new Order();
}
}
第三步:在Spring配置文件中指定factory-bean以及factory-method
<bean id="orderFactory" class="com.powernode.spring6.bean.OrderFactory"/>
<bean id="orderBean" factory-bean="orderFactory" factory-method="get"/>
5.4 FactoryBean接口实例化
在Spring中,当你编写的类直接实现FactoryBean接口之后,factory-bean不需要指定了,factory-method也不需要指定了。
factory-bean会自动指向实现FactoryBean接口的类,factory-method会自动指向getObject()方法。
第一步:定义一个Bean
public class Person {
}
第二步:编写一个类实现FactoryBean接口
public class PersonFactoryBean implements FactoryBean<Person> {
@Override
public Person getObject() throws Exception {
return new Person();
}
@Override
public Class<?> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
// true表示单例
// false表示原型
return true;
}
}
第三步:在Spring配置文件中配置FactoryBean
<bean id="personBean" class="com.powernode.spring6.bean.PersonFactoryBean"/>
FactoryBean在Spring中是一个接口。被称为“工厂Bean”。“工厂Bean”是一种特殊的Bean。所有的“工厂Bean”都是用来协助Spring框架来创建其他Bean对象的。
5.5 BeanFactory和FactoryBean的区别
BeanFactory: Spring IoC容器的顶级对象,BeanFactory被翻译为“Bean工厂”,在Spring的IoC容器中,“Bean工厂”负责创建Bean对象。
BeanFactory是工厂。
FactoryBean: 它是一个Bean,是一个能够辅助Spring实例化其它Bean对象的一个Bean。
5.6 注入自定义date
public class DateFactoryBean implements FactoryBean<Date> {
// 定义属性接收日期字符串
private String date;
// 通过构造方法给日期字符串属性赋值
public DateFactoryBean(String date) {
this.date = date;
}
@Override
public Date getObject() throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.parse(this.date);
}
@Override
public Class<?> getObjectType() {
return null;
}
}
<bean id="dateBean" class="com.powernode.spring6.bean.DateFactoryBean">
<constructor-arg name="date" value="1999-10-11"/>
</bean>
<!--之前方式生成日期对象
<bean id="studentBean" class="com.powernode.spring6.bean.Student">
<property name="birth" value="2002-10-10"/>
</bean>
-->
<bean id="studentBean" class="com.powernode.spring6.bean.Student">
<property name="birth" ref="dateBean"/>
</bean>
6 bean的生命周期
6.1 生命周期5步
● 第一步:实例化Bean
● 第二步:Bean属性赋值
● 第三步:初始化Bean
● 第四步:使用Bean
● 第五步:销毁Bean
public class User {
private String name;
public User() {
System.out.println("1.实例化Bean");
}
public void setName(String name) {
this.name = name;
System.out.println("2.Bean属性赋值");
}
public void initBean(){
System.out.println("3.初始化Bean");
}
public void destroyBean(){
System.out.println("5.销毁Bean");
}
}
<?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">
<!--
init-method属性指定初始化方法。
destroy-method属性指定销毁方法。
-->
<bean id="userBean" class="com.powernode.spring6.bean.User" init-method="initBean" destroy-method="destroyBean">
<property name="name" value="zhangsan"/>
</bean>
</beans>
public class BeanLifecycleTest {
@Test
public void testLifecycle(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User userBean = applicationContext.getBean("userBean", User.class);
System.out.println("4.使用Bean");
// 只有正常关闭spring容器才会执行销毁方法
ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
context.close();
}
}
总结
● 第一:只有正常关闭spring容器,bean的销毁方法才会被调用。
● 第二:ClassPathXmlApplicationContext类才有close()方法。
● 第三:配置文件中的init-method指定初始化方法。destroy-method指定销毁方法。
6.2 生命周期7步
在以上的5步中,第3步是初始化Bean,如果你还想在初始化前和初始化后添加代码,可以加入“Bean后处理器”。
编写一个类实现BeanPostProcessor类,并且重写before和after方法:
public class LogBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Bean后处理器的before方法执行,即将开始初始化");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Bean后处理器的after方法执行,已完成初始化");
return bean;
}
}
<!--配置Bean后处理器。这个后处理器将作用于当前配置文件中所有的bean。-->
<bean class="com.powernode.spring6.bean.LogBeanPostProcessor"/>
一定要注意:在spring.xml文件中配置的Bean后处理器将作用于当前配置文件中所有的Bean。
6.3 生命周期10步
上图中检查Bean是否实现了Aware的相关接口是什么意思?
Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
● 当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。
● 当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。
● 当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。
测试以上10步,可以让User类实现5个接口,并实现所有方法:
● BeanNameAware
● BeanClassLoaderAware
● BeanFactoryAware
● InitializingBean
● DisposableBean
package com.powernode.spring6.bean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
/**
* @author 动力节点
* @version 1.0
* @className User
* @since 1.0
**/
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
private String name;
public User() {
System.out.println("1.实例化Bean");
}
public void setName(String name) {
this.name = name;
System.out.println("2.Bean属性赋值");
}
public void initBean(){
System.out.println("6.初始化Bean");
}
public void destroyBean(){
System.out.println("10.销毁Bean");
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
System.out.println("3.类加载器:" + classLoader);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("3.Bean工厂:" + beanFactory);
}
@Override
public void setBeanName(String name) {
System.out.println("3.bean名字:" + name);
}
@Override
public void destroy() throws Exception {
System.out.println("9.DisposableBean destroy");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("5.afterPropertiesSet执行");
}
}
public class LogBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("4.Bean后处理器的before方法执行,即将开始初始化");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("7.Bean后处理器的after方法执行,已完成初始化");
return bean;
}
}
通过测试可以看出来:
● InitializingBean的方法早于init-method的执行。
● DisposableBean的方法早于destroy-method的执行。
对于SpringBean的生命周期,掌握之前的7步即可。够用。
6.4 作用域不同对生命周期的影响
Spring 根据Bean的作用域来选择管理方式。
● 对于singleton作用域的Bean,Spring 能够精确地知道该Bean何时被创建,何时初始化完成,以及何时被销毁;
● 而对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪其生命周期。
只执行了前8步,第9和10都没有执行。
6.5 自己new的对象如何让spring管理
public class User {
}
public class RegisterBeanTest {
@Test
public void testBeanRegister(){
// 自己new的对象
User user = new User();
System.out.println(user);
// 创建 默认可列表BeanFactory 对象
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 注册Bean
factory.registerSingleton("userBean", user);
// 从spring容器中获取bean
User userBean = factory.getBean("userBean", User.class);
System.out.println(userBean);
}
}
7 bean的循环依赖
7.1 什么是循环依赖
A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。
比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。
public class Husband {
private String name;
private Wife wife;
}
public class Wife {
private String name;
private Husband husband;
}
7.2 singleton下的set注入的循环依赖
public class Husband {
private String name;
private Wife wife;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setWife(Wife wife) {
this.wife = wife;
}
// toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢出错误。
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife.getName() +
'}';
}
}
public class Wife {
private String name;
private Husband husband;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setHusband(Husband husband) {
this.husband = husband;
}
// toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband.getName() +
'}';
}
}
<?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="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton">
<property name="name" value="小花"/>
<property name="husband" ref="husbandBean"/>
</bean>
</beans>
通过测试得知:在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。
7.3 prototype下的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/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="prototype">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="prototype">
<property name="name" value="小花"/>
<property name="husband" ref="husbandBean"/>
</bean>
</beans>
执行测试程序:发生了异常,异常信息如下:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'husbandBean': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:265)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
... 44 more
1 通过测试得知,当循环依赖的所有Bean的scope="prototype"的时候,产生的循环依赖,Spring是无法解决的,会出现BeanCurrentlyInCreationException异常。
2 以上两个Bean,如果其中一个是singleton,另一个是prototype,是没有问题的。
7.4 singleton下构造方法注入的循环依赖
public class Husband {
private String name;
private Wife wife;
public Husband(String name, Wife wife) {
this.name = name;
this.wife = wife;
}
// -----------------------分割线--------------------------------
public String getName() {
return name;
}
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife +
'}';
}
}
public class Wife {
private String name;
private Husband husband;
public Wife(String name, Husband husband) {
this.name = name;
this.husband = husband;
}
// -------------------------分割线--------------------------------
public String getName() {
return name;
}
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband +
'}';
}
}
<?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="hBean" class="com.powernode.spring6.bean2.Husband" scope="singleton">
<constructor-arg name="name" value="张三"/>
<constructor-arg name="wife" ref="wBean"/>
</bean>
<bean id="wBean" class="com.powernode.spring6.bean2.Wife" scope="singleton">
<constructor-arg name="name" value="小花"/>
<constructor-arg name="husband" ref="hBean"/>
</bean>
</beans>
执行结果:发生了异常,信息如下:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'hBean': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:227)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
... 56 more
主要原因是因为通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。
7.5 spring解决循环依赖的机理
根本的原因在于:这种方式可以做到将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。
实例化Bean的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界。
给Bean属性赋值的时候:调用setter方法来完成。
在以上类中包含三个重要的属性:
Cache of singleton objects: bean name to bean instance. 单例对象的缓存:key存储bean名称,value存储Bean对象【一级缓存】
Cache of early singleton objects: bean name to bean instance. 早期单例对象的缓存:key存储bean名称,value存储早期的Bean对象【二级缓存】
Cache of singleton factories: bean name to ObjectFactory. 单例工厂缓存:key存储bean名称,value存储该Bean对应的ObjectFactory对象【三级缓存】
总结:
Spring只能解决setter方法注入的单例bean之间的循环依赖。ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。
8 Spring IOC注解开发
负责声明Bean的注解,常见的包括四个:
● @Component
● @Controller
● @Service
● @Repository
8.1 spring 注解的使用
● 第一步:加入aop的依赖
● 第二步:在配置文件中添加context命名空间
● 第三步:在配置文件中指定扫描的包
● 第四步:在Bean类上使用注解
第一步:加入aop的依赖
我们可以看到当加入spring-context依赖之后,会关联加入aop的依赖。所以这一步不用做。
第二步:在配置文件中添加context命名空间
<?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">
</beans>
第三步:在配置文件中指定要扫描的包
<?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="com.powernode.spring6.bean"/>
</beans>
第四步:在Bean类上使用注解
@Component("userBean")
//Component(value = "userBean")
public class User {
}
第五步:测试
public class AnnotationTest {
@Test
public void testBean(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User userBean = applicationContext.getBean("userBean", User.class);
System.out.println(userBean);
}
}
问题1:如果把value属性彻底去掉,spring会被Bean自动取名吗?
会的。并且默认名字的规律是:Bean类名首字母小写即可。
问题2:在配置文件中如何指定扫描多个包
使用逗号分隔,如下所示:
<context:component-scan base-package="com.powernode.spring6.bean,com.powernode.spring6.bean2"/>
8.2 选择性实例化bean
假设在某个包下有很多Bean,有的Bean上标注了Component,有的标注了Controller,有的标注了Service,有的标注了Repository,现在由于某种特殊业务的需要,只允许其中所有的Controller参与Bean管理,其他的都不实例化。这应该怎么办呢?
<?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="com.powernode.spring6.bean3" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
use-default-filters="true" 表示:使用spring默认的规则,只要有Component、Controller、Service、Repository中的任意一个注解标注,则进行实例化。
use-default-filters="false" 表示:不再spring默认实例化规则,即使有Component、Controller、Service、Repository这些注解标注,也不再实例化。
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> 表示只有Controller进行实例化。
也可以将use-default-filters设置为true(不写就是true),并且采用exclude-filter方式排出哪些注解标注的Bean不参与实例化:
<context:component-scan base-package="com.powernode.spring6.bean3">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
8.3 负责注入的注解
@Component @Controller @Service @Repository 这四个注解是用来声明Bean的,声明后这些Bean将被实例化。接下来我们看一下,如何给Bean的属性赋值。给Bean属性赋值需要用到这些注解:
● @Value
● @Autowired
● @Qualifier
● @Resource
8.3.1 @Value
当属性的类型是简单类型时,可以使用@Value注解进行注入。
使用在属性上
@Component
public class User {
@Value(value = "zhangsan")
private String name;
@Value("20")
private int age;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
使用在set方法上
@Component
public class User {
private String name;
private int age;
@Value("李四")
public void setName(String name) {
this.name = name;
}
@Value("30")
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
使用在构造方法上
@Component
public class User {
private String name;
private int age;
public User(@Value("隔壁老王") String name, @Value("33") int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
8.3.2 @Autowired
@Autowired注解可以用来注入非简单类型。被翻译为:自动连线的,或者自动装配。单独使用@Autowired注解,默认根据类型装配。【默认是byType】
源码中有两处需要注意:
该注解有一个required属性,默认值是true,表示在注入的时候要求被注入的Bean必须是存在的,如果不存在则报错。如果required属性设置为false,表示注入的Bean存在或者不存在都没关系,存在的话就注入,不存在的话,也不报错。
属性上使用:
@Service // 纳入bean管理
public class UserService {
@Autowired // 在属性上注入
private UserDao userDao;
// 没有提供构造方法和setter方法。
public void save(){
userDao.insert();
}
}
setter方法上使用:
@Service
public class UserService {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
构造方法上使用:
@Service
public class UserService {
private UserDao userDao;
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
构造方法的形参上使用
@Service
public class UserService {
private UserDao userDao;
public UserService(@Autowired UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
如果有多个相同的属性出现,通过Autowired就会报错,解决的话需要使用下面的@Qualifier 来指定名称
8.3.3 @Qualifier
@Service
public class UserService {
private UserDao userDao;
@Autowired
@Qualifier("userDaoForOracle") // 这个是bean的名字。
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
总结:
● @Autowired注解可以出现在:属性上、构造方法上、构造方法的参数上、setter方法上。
● 当带参数的构造方法只有一个,@Autowired注解可以省略。
● @Autowired注解默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier注解一起使用。
8.3.4 @Resource
● @Resource注解默认根据名称装配byName,未指定name时,使用属性名作为name。通过name找不到的话会自动启动通过类型byType装配。
● @Autowired注解默认根据类型装配byType,如果想根据名称装配,需要配合@Qualifier注解一起用。
● @Resource注解用在属性上、setter方法上。
● @Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖:【如果是JDK8的话不需要额外引入依赖。高于JDK11或低于JDK8需要引入以下依赖。】
如果你是Spring6+版本请使用这个依赖
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
如果你是spring5-版本请使用这个依赖
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
在属性上使用:
@Service
public class UserService {
//当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。
@Resource(name = "xyz")
private UserDao userDao;
public void save(){
userDao.insert();
}
}
在set方法上使用:
@Service
public class UserService {
private UserDao userDao;
//@Resource
@Resource(name = "userDaoForMySQL")
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
一句话总结@Resource注解:默认byName注入,没有指定name时把属性名当做name,根据name找不到时,才会byType注入。byType注入时,某种类型的Bean只能有一个。
8.3.5 全注解开发
所谓的全注解开发就是不再使用spring配置文件了。写一个配置类来代替配置文件。
@Configuration
@ComponentScan({"com.powernode.spring6.dao", "com.powernode.spring6.service"})
public class Spring6Configuration {
}
编写测试程序:不再new ClassPathXmlApplicationContext()对象了。
@Test
public void testNoXml(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.save();
}
9 JdbcTemplate
9.1 环境准备
数据库表:t_user
引入相关依赖:
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>spring6-007-jdbc</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<repositories>
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!--新增的依赖:mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--新增的依赖:spring jdbc,这个依赖中有JdbcTemplate-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.0-M2</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
准备实体类:表t_user对应的实体类User。
public class User {
private Integer id;
private String realName;
private Integer age;
@Override
public String toString() {
return "User{" +
"id=" + id +
", realName='" + realName + '\'' +
", age=" + age +
'}';
}
public User() {
}
public User(Integer id, String realName, Integer age) {
this.id = id;
this.realName = realName;
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
创建数据源
public class MyDataSource implements DataSource {
// 添加4个属性
private String driver;
private String url;
private String username;
private String password;
// 提供4个setter方法
public void setDriver(String driver) {
this.driver = driver;
}
public void setUrl(String url) {
this.url = url;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
// 重点写怎么获取Connection对象就行。其他方法不用管。
@Override
public Connection getConnection() throws SQLException {
try {
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, username, password);
return conn;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}
创建jdbcTemplate对象
<?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="myDataSource" class="com.powernode.spring6.jdbc.MyDataSource">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="myDataSource"/>
</bean>
</beans>
9.2 新增
public class JdbcTest {
@Test
public void testInsert(){
// 获取JdbcTemplate对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
// 执行插入操作
// 注意:insert delete update的sql语句,都是执行update方法。
String sql = "insert into t_user(id,real_name,age) values(?,?,?)";
int count = jdbcTemplate.update(sql, null, "张三", 30);
System.out.println("插入的记录条数:" + count);
}
}
9.3 修改
@Test
public void testUpdate(){
// 获取JdbcTemplate对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
// 执行更新操作
String sql = "update t_user set real_name = ?, age = ? where id = ?";
int count = jdbcTemplate.update(sql, "张三丰", 55, 1);
System.out.println("更新的记录条数:" + count);
}
9.4 删除
@Test
public void testDelete(){
// 获取JdbcTemplate对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
// 执行delete
String sql = "delete from t_user where id = ?";
int count = jdbcTemplate.update(sql, 1);
System.out.println("删除了几条记录:" + count);
}
9.5 查询一个对象
@Test
public void testSelectOne(){
// 获取JdbcTemplate对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
// 执行select
String sql = "select id, real_name, age from t_user where id = ?";
User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 2);
System.out.println(user);
}
queryForObject方法三个参数:
● 第一个参数:sql语句
● 第二个参数:Bean属性值和数据库记录行的映射对象。在构造方法中指定映射的对象类型。
● 第三个参数:可变长参数,给sql语句的占位符问号传值。
9.6 查询多个对象
@Test
public void testSelectAll(){
// 获取JdbcTemplate对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
// 执行select
String sql = "select id, real_name, age from t_user";
List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
System.out.println(users);
}
9.7 查询一个值
@Test
public void testSelectOneValue(){
// 获取JdbcTemplate对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
// 执行select
String sql = "select count(1) from t_user";
Integer count = jdbcTemplate.queryForObject(sql, int.class); // 这里用Integer.class也可以
System.out.println("总记录条数:" + count);
}
9.8 批量添加
@Test
public void testAddBatch(){
// 获取JdbcTemplate对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
// 批量添加
String sql = "insert into t_user(id,real_name,age) values(?,?,?)";
Object[] objs1 = {null, "小花", 20};
Object[] objs2 = {null, "小明", 21};
Object[] objs3 = {null, "小刚", 22};
List<Object[]> list = new ArrayList<>();
list.add(objs1);
list.add(objs2);
list.add(objs3);
int[] count = jdbcTemplate.batchUpdate(sql, list);
System.out.println(Arrays.toString(count));
}
9.9 批量修改
@Test
public void testUpdateBatch(){
// 获取JdbcTemplate对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
// 批量修改
String sql = "update t_user set real_name = ?, age = ? where id = ?";
Object[] objs1 = {"小花11", 10, 2};
Object[] objs2 = {"小明22", 12, 3};
Object[] objs3 = {"小刚33", 9, 4};
List<Object[]> list = new ArrayList<>();
list.add(objs1);
list.add(objs2);
list.add(objs3);
int[] count = jdbcTemplate.batchUpdate(sql, list);
System.out.println(Arrays.toString(count));
}
9.10 批量删除
@Test
public void testDeleteBatch(){
// 获取JdbcTemplate对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
// 批量删除
String sql = "delete from t_user where id = ?";
Object[] objs1 = {2};
Object[] objs2 = {3};
Object[] objs3 = {4};
List<Object[]> list = new ArrayList<>();
list.add(objs1);
list.add(objs2);
list.add(objs3);
int[] count = jdbcTemplate.batchUpdate(sql, list);
System.out.println(Arrays.toString(count));
}
9.11 使用回调函数
@Test
public void testCallback(){
// 获取JdbcTemplate对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
String sql = "select id, real_name, age from t_user where id = ?";
User user = jdbcTemplate.execute(sql, new PreparedStatementCallback<User>() {
@Override
public User doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException {
User user = null;
ps.setInt(1, 5);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
user = new User();
user.setId(rs.getInt("id"));
user.setRealName(rs.getString("real_name"));
user.setAge(rs.getInt("age"));
}
return user;
}
});
System.out.println(user);
}
9.12 使用德鲁伊连接池
第一步:引入德鲁伊连接池的依赖。(毕竟是别人写的)
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.8</version>
</dependency>
第二步:将德鲁伊中的数据源配置到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="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource"/>
</bean>
</beans>
10 代理模式
10.1 静态代理
现在有这样一个接口和实现类:
public interface OrderService {
/**
* 生成订单
*/
void generate();
/**
* 查看订单详情
*/
void detail();
/**
* 修改订单
*/
void modify();
}
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
try {
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成");
}
@Override
public void detail() {
try {
Thread.sleep(2541);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单信息如下:******");
}
@Override
public void modify() {
try {
Thread.sleep(1010);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改");
}
}
需求:统计每个业务方法所耗费的时长
第一种方案:直接修改Java源代码,在每个业务方法中添加统计逻辑
第二种方案:编写一个子类继承OrderServiceImpl,在子类中重写每个方法
第三种方案:使用代理模式(这里采用静态代理)
public class OrderServiceProxy implements OrderService{ // 代理对象
// 目标对象
private OrderService orderService;
// 通过构造方法将目标对象传递给代理对象
public OrderServiceProxy(OrderService orderService) {
this.orderService = orderService;
}
@Override
public void generate() {
long begin = System.currentTimeMillis();
// 执行目标对象的目标方法
orderService.generate();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
@Override
public void detail() {
long begin = System.currentTimeMillis();
// 执行目标对象的目标方法
orderService.detail();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
@Override
public void modify() {
long begin = System.currentTimeMillis();
// 执行目标对象的目标方法
orderService.modify();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
}
大家思考一下:如果系统中业务接口很多,一个接口对应一个代理类,显然也是不合理的,会导致类爆炸。怎么解决这个问题?动态代理可以解决。因为在动态代理中可以在内存中动态的为我们生成代理类的字节码。代理类不需要我们写了。类爆炸解决了,而且代码只需要写一次,代码也会得到复用。
10.2 动态代理
10.2.1 JDK动态代理
一个接口和一个实现类。
public interface OrderService {
/**
* 生成订单
*/
void generate();
/**
* 查看订单详情
*/
void detail();
/**
* 修改订单
*/
void modify();
}
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
try {
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成");
}
@Override
public void detail() {
try {
Thread.sleep(2541);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单信息如下:******");
}
@Override
public void modify() {
try {
Thread.sleep(1010);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改");
}
}
客户端程序
public class Client {
public static void main(String[] args) {
// 创建目标对象
OrderService target = new OrderServiceImpl();
// 创建代理对象
OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TimerInvocationHandler(target));
// 调用代理对象的代理方法
orderServiceProxy.detail();
orderServiceProxy.modify();
orderServiceProxy.generate();
}
}
java.lang.reflect.InvocationHandler接口的实现类
public class TimerInvocationHandler implements InvocationHandler {
// 目标对象
private Object target;
// 通过构造方法来传目标对象
public TimerInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 目标执行之前增强。
long begin = System.currentTimeMillis();
// 调用目标对象的目标方法
Object retValue = method.invoke(target, args);
// 目标执行之后增强。
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
// 一定要记得返回哦。
return retValue;
}
}
10.2.2 CGLIB动态代理
CGLIB既可以代理接口,又可以代理类。底层采用继承的方式实现。所以被代理的目标类不能使用final修饰。
引入依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
准备一个没有实现接口的类,如下:
public class UserService {
public void login(){
System.out.println("用户正在登录系统....");
}
public void logout(){
System.out.println("用户正在退出系统....");
}
}
使用CGLIB在内存中为UserService类生成代理类,并创建对象:
public class Client {
public static void main(String[] args) {
// 创建字节码增强器
Enhancer enhancer = new Enhancer();
// 告诉cglib要继承哪个类
enhancer.setSuperclass(UserService.class);
// 设置回调接口
enhancer.setCallback(new TimerMethodInterceptor());
// 生成源码,编译class,加载到JVM,并创建代理对象
UserService userServiceProxy = (UserService)enhancer.create();
userServiceProxy.login();
userServiceProxy.logout();
}
}
编写MethodInterceptor接口实现类:
public class TimerMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 前增强
long begin = System.currentTimeMillis();
// 调用目标
Object retValue = methodProxy.invokeSuper(target, objects);
// 后增强
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - begin) + "毫秒");
// 一定要返回
return retValue;
}
}
对于高版本的JDK,如果使用CGLIB,需要在启动项中添加两个启动参数:
● --add-opens java.base/java.lang=ALL-UNNAMED
● --add-opens java.base/sun.net.util=ALL-UNNAMED
11 切面编程AOP
11. 1 AOP七大术语
public class UserService{
public void do1(){
System.out.println("do 1");
}
public void do2(){
System.out.println("do 2");
}
public void do3(){
System.out.println("do 3");
}
public void do4(){
System.out.println("do 4");
}
public void do5(){
System.out.println("do 5");
}
// 核心业务方法
public void service(){
do1();
do2();
do3();
do5();
}
}
● 连接点 Joinpoint
○ 目标对象中,所有可以增强的方法
● 切点 Pointcut
○ 目标对象,已经增强的方法
● 通知 Advice
○ 增强的代码
○ 通知包括:
■ 前置通知
■ 后置通知
■ 环绕通知
■ 异常通知
■ 最终通知
● 切面 Aspect
○ 切点 + 通知就是切面。
● 织入 Weaving
○ 把通知应用到目标对象上的过程。
● 代理对象 Proxy
○ 一个目标对象被织入通知后产生的新对象。
● 目标对象 Target
○ 被织入通知的对象。
11.2 切点表达式
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
11.3 使用spring的AOP
Spring对AOP的实现包括以下3种方式:
● 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式。
● 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式。
● 第三种方式:Spring框架自己实现的AOP,基于XML配置方式。
11.3.1 准备工作
使用Spring+AspectJ的AOP需要引入的依赖如下:
<!--spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring aop依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring aspects依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.0-M2</version>
</dependency>
11.3.2 基于AspectJ的AOP注解式开发
执行步骤
第一步:定义目标类以及目标方法
// 目标类
public class OrderService {
// 目标方法
public void generate(){
System.out.println("订单已生成!");
}
}
第二步:定义切面类
// 切面类
@Aspect
public class MyAspect {
}
第三步:目标类和切面类都纳入spring bean管理
在目标类OrderService上添加@Component注解。
在切面类MyAspect类上添加@Component注解。
第四步:在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"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
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
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启组件扫描-->
<context:component-scan base-package="com.powernode.spring6.service"/>
</beans>
第五步:在切面类中添加通知
// 切面类
@Aspect
@Component
public class MyAspect {
// 这就是需要增强的代码(通知)
public void advice(){
System.out.println("我是一个通知");
}
}
第六步:在通知上添加切点表达式
// 切面类
@Aspect
@Component
public class MyAspect {
// 切点表达式
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
// 这就是需要增强的代码(通知)
public void advice(){
System.out.println("我是一个通知");
}
}
第七步:在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"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
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
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启组件扫描-->
<context:component-scan base-package="com.powernode.spring6.service"/>
<!--开启自动代理-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
<aop:aspectj-autoproxy proxy-target-class="true"/> 开启自动代理之后,凡事带有@Aspect注解的bean都会生成代理对象。
proxy-target-class="true" 表示采用cglib动态代理。
proxy-target-class="false" 表示采用jdk动态代理。默认值是false。即使写成false,当没有接口的时候,也会自动选择cglib生成代理类。
通知类型
● 前置通知:@Before 目标方法执行之前的通知
● 后置通知:@AfterReturning 目标方法执行之后的通知
● 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知。
● 异常通知:@AfterThrowing 发生异常之后执行的通知
● 最终通知:@After 放在finally语句块中的通知
// 切面类
@Component
@Aspect
public class MyAspect {
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
// 执行目标方法。
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void beforeAdvice(){
System.out.println("前置通知");
}
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知");
}
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知");
}
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterAdvice(){
System.out.println("最终通知");
}
}
// 目标类
@Component
public class OrderService {
// 目标方法
public void generate(){
System.out.println("订单已生成!");
}
}
执行顺序
如果出现异常
通过测试得知,当发生异常之后,最终通知也会执行,因为最终通知@After会出现在finally语句块中。
出现异常之后,后置通知和环绕通知的结束部分不会执行。
切面的先后顺序
@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高。
@Aspect
@Component
@Order(1) //设置优先级
public class YourAspect {
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("YourAspect环绕通知开始");
// 执行目标方法。
proceedingJoinPoint.proceed();
System.out.println("YourAspect环绕通知结束");
}
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void beforeAdvice(){
System.out.println("YourAspect前置通知");
}
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterReturningAdvice(){
System.out.println("YourAspect后置通知");
}
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("YourAspect异常通知");
}
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterAdvice(){
System.out.println("YourAspect最终通知");
}
}
// 切面类
@Component
@Aspect
@Order(2) //设置优先级
public class MyAspect {
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
// 执行目标方法。
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void beforeAdvice(){
System.out.println("前置通知");
}
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知");
}
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知");
}
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterAdvice(){
System.out.println("最终通知");
}
}
执行测试程序
通过修改@Order注解的整数值来切换顺序,执行测试程序:
优化使用切点表达式
// 切面类
@Component
@Aspect
@Order(2)
public class MyAspect {
@Pointcut("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void pointcut(){}
@Around("pointcut()")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
// 执行目标方法。
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
@Before("pointcut()")
public void beforeAdvice(){
System.out.println("前置通知");
}
@AfterReturning("pointcut()")
public void afterReturningAdvice(){
System.out.println("后置通知");
}
@AfterThrowing("pointcut()")
public void afterThrowingAdvice(){
System.out.println("异常通知");
}
@After("pointcut()")
public void afterAdvice(){
System.out.println("最终通知");
}
}
全注解式开发AOP
@Configuration
@ComponentScan("com.powernode.spring6.service")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Spring6Configuration {
}
@Test
public void testAOPWithAllAnnotation(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
11.3.3 基于XML配置方式的AOP
第一步:编写目标类
// 目标类
public class VipService {
public void add(){
System.out.println("保存vip信息。");
}
}
第二步:编写切面类,并且编写通知
// 负责计时的切面类
public class TimerAspect {
public void time(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long begin = System.currentTimeMillis();
//执行目标
proceedingJoinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
}
第三步:编写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"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
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
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--纳入spring bean管理-->
<bean id="vipService" class="com.powernode.spring6.service.VipService"/>
<bean id="timerAspect" class="com.powernode.spring6.service.TimerAspect"/>
<!--aop配置-->
<aop:config>
<!--切点表达式-->
<aop:pointcut id="p" expression="execution(* com.powernode.spring6.service.VipService.*(..))"/>
<!--切面-->
<aop:aspect ref="timerAspect">
<!--切面=通知 + 切点-->
<aop:around method="time" pointcut-ref="p"/>
</aop:aspect>
</aop:config>
</beans>
11.4 AOP:事务处理
@Component
// 业务类
public class AccountService {
// 转账业务方法
public void transfer(){
System.out.println("正在进行银行账户转账");
}
// 取款业务方法
public void withdraw(){
System.out.println("正在进行取款操作");
}
}
@Component
// 业务类
public class OrderService {
// 生成订单
public void generate(){
System.out.println("正在生成订单");
}
// 取消订单
public void cancel(){
System.out.println("正在取消订单");
}
}
注意,以上两个业务类已经纳入spring bean的管理,因为都添加了@Component注解。
接下来我们给以上两个业务类的4个方法添加事务控制代码,使用AOP来完成:
@Aspect
@Component
// 事务切面类
public class TransactionAspect {
@Around("execution(* com.powernode.spring6.biz..*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
try {
System.out.println("开启事务");
// 执行目标
proceedingJoinPoint.proceed();
System.out.println("提交事务");
} catch (Throwable e) {
System.out.println("回滚事务");
}
}
}
测试
public class AOPTest2 {
@Test
public void testTransaction(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
// 生成订单
orderService.generate();
// 取消订单
orderService.cancel();
// 转账
accountService.transfer();
// 取款
accountService.withdraw();
}
}
执行结果:
11.5 AOP:安全日志
需求是这样的:项目开发结束了,已经上线了。运行正常。客户提出了新的需求:凡事在系统中进行修改操作的,删除操作的,新增操作的,都要把这个人记录下来。
@Component
//用户业务
public class UserService {
public void getUser(){
System.out.println("获取用户信息");
}
public void saveUser(){
System.out.println("保存用户");
}
public void deleteUser(){
System.out.println("删除用户");
}
public void modifyUser(){
System.out.println("修改用户");
}
}
// 商品业务类
@Component
public class ProductService {
public void getProduct(){
System.out.println("获取商品信息");
}
public void saveProduct(){
System.out.println("保存商品");
}
public void deleteProduct(){
System.out.println("删除商品");
}
public void modifyProduct(){
System.out.println("修改商品");
}
}
注意:已经添加了@Component注解。
接下来我们使用aop来解决上面的需求:编写一个负责安全的切面类
@Component
@Aspect
public class SecurityAspect {
@Pointcut("execution(* com.powernode.spring6.biz..save*(..))")
public void savePointcut(){}
@Pointcut("execution(* com.powernode.spring6.biz..delete*(..))")
public void deletePointcut(){}
@Pointcut("execution(* com.powernode.spring6.biz..modify*(..))")
public void modifyPointcut(){}
@Before("savePointcut() || deletePointcut() || modifyPointcut()")
public void beforeAdivce(JoinPoint joinpoint){
System.out.println("XXX操作员正在操作"+joinpoint.getSignature().getName()+"方法");
}
}
@Test
public void testSecurity(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
UserService userService = applicationContext.getBean("userService", UserService.class);
ProductService productService = applicationContext.getBean("productService", ProductService.class);
userService.getUser();
userService.saveUser();
userService.deleteUser();
userService.modifyUser();
productService.getProduct();
productService.saveProduct();
productService.deleteProduct();
productService.modifyProduct();
}
执行结果:
12 Sprin对事务的支持
12.1 引入
pom 依赖
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>spring6-013-tx-bank</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!--仓库-->
<repositories>
<!--spring里程碑版本的仓库-->
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<!--依赖-->
<dependencies>
<!--spring context-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring jdbc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--德鲁伊连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.13</version>
</dependency>
<!--@Resource注解-->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
第一步:准备数据库表
表结构:
表数据:
第二步:创建包结构
com.powernode.bank.pojo
com.powernode.bank.service
com.powernode.bank.service.impl
com.powernode.bank.dao
com.powernode.bank.dao.impl
第三步:准备POJO类
public class Account {
private String actno;
private Double balance;
@Override
public String toString() {
return "Account{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
public Account() {
}
public Account(String actno, Double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
}
第四步:编写持久层
public interface AccountDao {
/**
* 根据账号查询余额
* @param actno
* @return
*/
Account selectByActno(String actno);
/**
* 更新账户
* @param act
* @return
*/
int update(Account act);
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
@Resource(name = "jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@Override
public Account selectByActno(String actno) {
String sql = "select actno, balance from t_act where actno = ?";
Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);
return account;
}
@Override
public int update(Account act) {
String sql = "update t_act set balance = ? where actno = ?";
int count = jdbcTemplate.update(sql, act.getBalance(), act.getActno());
return count;
}
}
第五步:编写业务层
public interface AccountService {
/**
* 转账
* @param fromActno
* @param toActno
* @param money
*/
void transfer(String fromActno, String toActno, double money);
}
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
public void transfer(String fromActno, String toActno, double money) {
// 查询账户余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new RuntimeException("账户余额不足");
}
// 余额充足,开始转账
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.update(fromAct);
count += accountDao.update(toAct);
if (count != 2) {
throw new RuntimeException("转账失败,请联系银行");
}
}
}
第六步:编写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"
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="com.powernode.bank"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
第七步:编写表示层(测试程序)
public class BankTest {
@Test
public void testTransfer(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
try {
accountService.transfer("act-001", "act-002", 10000);
System.out.println("转账成功");
} catch (Exception e) {
e.printStackTrace();
}
}
}
数据变化:
模拟异常
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
public void transfer(String fromActno, String toActno, double money) {
// 查询账户余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new RuntimeException("账户余额不足");
}
// 余额充足,开始转账
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.update(fromAct);
// 模拟异常
String s = null;
s.toString();
count += accountDao.update(toAct);
if (count != 2) {
throw new RuntimeException("转账失败,请联系银行");
}
}
}
丢了1万。
12.2 Spring 对事务的支持
Spring实现事务的两种方式
● 编程式事务
○ 通过编写代码的方式来实现事务的管理。
● 声明式事务
○ 基于注解方式
○ 基于XML配置方式
声明式事务之注解实现方式
第一步:在spring配置文件中配置事务管理器。
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
第二步:在spring配置文件中引入tx命名空间。
<?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"
xmlns:tx="http://www.springframework.org/schema/tx"
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
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
第三步:在spring配置文件中配置“事务注解驱动器”,开始注解的方式控制事务。
<tx:annotation-driven transaction-manager="transactionManager"/>
第四步:在service类上或方法上添加@Transactional注解
在类上添加该注解,该类中所有的方法都有事务。在某个方法上添加该注解,表示只有这个方法使用事务。
@Service("accountService")
@Transactional
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
public void transfer(String fromActno, String toActno, double money) {
// 查询账户余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new RuntimeException("账户余额不足");
}
// 余额充足,开始转账
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.update(fromAct);
// 模拟异常
String s = null;
s.toString();
count += accountDao.update(toAct);
if (count != 2) {
throw new RuntimeException("转账失败,请联系银行");
}
}
}
12.3 事务的属性
事务中的重点属性:
● 事务传播行为
● 事务隔离级别
● 事务超时
● 只读事务
● 设置出现哪些异常回滚事务
● 设置出现哪些异常不回滚事务
事务传播行为
在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。
一共有七种传播行为:
● REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
● SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】
● MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】
● REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】
● NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】
● NEVER:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】
● NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
在代码中设置事务的传播行为:
@Transactional(propagation = Propagation.REQUIRED)
可以编写程序测试一下传播行为:
1号service
@Transactional(propagation = Propagation.REQUIRED)
public void save(Account act) {
// 这里调用dao的insert方法。
accountDao.insert(act); // 保存act-003账户
// 创建账户对象
Account act2 = new Account("act-004", 1000.0);
try {
accountService.save(act2); // 保存act-004账户
} catch (Exception e) {
}
// 继续往后进行我当前1号事务自己的事儿。
}
2号service
@Override
//@Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Account act) {
accountDao.insert(act);
// 模拟异常
String s = null;
s.toString();
// 事儿没有处理完,这个大括号当中的后续也许还有其他的DML语句。
}
1 如果1号service和2号service都使用Propagation.REQUIRED,那么他们在同一个事务中,如下所示:
1 如果2号service抛出异常,1号service不捕获2号抛出的异常,由于在同一个事务中,所以保持不变
2 如果2号service抛出异常,在1号service中捕获了2号service的异常,由于在同一个事务中,所以不发生变化
如果2号service使用Propagation.REQUIRES_NEW,如下图所示
1 如果2号service抛出异常,1号service不捕获2号抛出的异常,由于不在同一个事务中,而且都抛出了异常,所以都回滚
2 如果2号service抛出异常,在1号service中捕获了2号service的异常,由于不在同一个事务中,所以1号不回滚,2号回滚
12.4 事务的隔离级别
事务隔离级别类似于教室A和教室B之间的那道墙,隔离级别越高表示墙体越厚。隔音效果越好。
数据库中读取数据存在的三大问题:(三大读问题)
● 脏读:读取到没有提交到数据库的数据,叫做脏读(读到了内存中的数据了)。
● 不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样。
● 幻读:读到的数据是假的。
事务隔离级别包括四个级别:
● 读未提交:READ_UNCOMMITTED
○ 这种隔离级别,存在脏读问题,所谓的脏读(dirty read)表示能够读取到其它事务未提交的数据。
● 读提交:READ_COMMITTED
○ 解决了脏读问题,其它事务提交之后才能读到,但存在不可重复读问题。
● 可重复读:REPEATABLE_READ
○ 解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的。但存在幻读问题。
● 序列化:SERIALIZABLE
○ 解决了幻读问题,事务排队执行。不支持并发。
@Transactional(isolation = Isolation.READ_COMMITTED)
一个service负责插入,一个service负责查询。负责插入的service要模拟延迟。
@Service("i1")
public class IsolationService1 {
@Resource(name = "accountDao")
private AccountDao accountDao;
// 1号
// 负责查询
// 当前事务可以读取到别的事务没有提交的数据。
//@Transactional(isolation = Isolation.READ_UNCOMMITTED)
// 对方事务提交之后的数据我才能读取到。
@Transactional(isolation = Isolation.READ_COMMITTED)
public void getByActno(String actno) {
Account account = accountDao.selectByActno(actno);
System.out.println("查询到的账户信息:" + account);
}
}
@Service("i2")
public class IsolationService2 {
@Resource(name = "accountDao")
private AccountDao accountDao;
// 2号
// 负责insert
@Transactional
public void save(Account act) {
accountDao.insert(act);
// 睡眠一会
try {
Thread.sleep(1000 * 20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试程序
@Test
public void testIsolation1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
IsolationService1 i1 = applicationContext.getBean("i1", IsolationService1.class);
i1.getByActno("act-004");
}
@Test
public void testIsolation2(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
IsolationService2 i2 = applicationContext.getBean("i2", IsolationService2.class);
Account act = new Account("act-004", 1000.0);
i2.save(act);
}
12.5 事务超时
@Transactional(timeout = 10)
以上代码表示设置事务的超时时间为10秒。
表示超过10秒如果该事务中所有的DML语句还没有执行完毕的话,最终结果会选择回滚。
默认值-1,表示没有时间限制。
这里有个坑,事务的超时时间指的是哪段时间?
在当前事务当中,最后一条DML语句执行之前的时间。如果最后一条DML语句后面很有很多业务逻辑,这些业务代码执行的时间不被计入超时时间。
以下代码的超时不会被计入超时时间
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
accountDao.insert(act);
// 睡眠一会
try {
Thread.sleep(1000 * 15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
以下代码超时时间会被计入超时时间
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
// 睡眠一会
try {
Thread.sleep(1000 * 15);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.insert(act);
}
12.6 只读事务
@Transactional(readOnly = true)
将当前事务设置为只读事务,在该事务执行过程中只允许select语句执行,delete insert update均不可执行。
该特性的作用是:启动spring的优化策略。提高select语句执行效率。
如果该事务中确实没有增删改操作,建议设置为只读事务。
12.7 设置哪些异常回滚事务
@Transactional(rollbackFor = RuntimeException.class)
表示只有发生RuntimeException异常或该异常的子类异常才回滚。
12.8 设置哪些异常不回滚事务
@Transactional(noRollbackFor = NullPointerException.class)
表示发生NullPointerException或该异常的子类异常不回滚,其他异常则回滚。
12.9 事务的全注解式开发
@Configuration
@ComponentScan("com.powernode.bank")
@EnableTransactionManagement
public class Spring6Config {
@Bean
public DataSource getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/spring6");
dataSource.setUsername("root");
dataSource.setPassword("root");
return dataSource;
}
@Bean(name = "jdbcTemplate")
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
12.10 声明式事务之XML实现方式
配置步骤:
● 第一步:配置事务管理器
● 第二步:配置通知
● 第三步:配置切面
记得添加aspectj的依赖:
<!--aspectj依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.0-M2</version>
</dependency>
Spring配置文件如下:
记得添加aop的命名空间。
<?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"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
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
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.powernode.bank"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置通知-->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
<tx:method name="del*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
<tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
<tx:method name="transfer*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
</tx:attributes>
</tx:advice>
<!--配置切面-->
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.powernode.bank.service..*(..))"/>
<!--切面 = 通知 + 切点-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
</beans>
13 Spring6整合JUnit5
13.1 Spring对JUnit4的支持
准备工作:
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>spring6-015-junit</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!--仓库-->
<repositories>
<!--spring里程碑版本的仓库-->
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<!--spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring对junit的支持相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--junit4依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
声明bean
@Component
public class User {
@Value("张三")
private String name;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User() {
}
public User(String name) {
this.name = name;
}
}
spring.xml
<?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="com.powernode.spring6.bean"/>
</beans>
单元测试:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringJUnit4Test {
@Autowired
private User user;
@Test
public void testUser(){
System.out.println(user.getName());
}
}
Spring提供的方便主要是这几个注解:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
在单元测试类上使用这两个注解之后,在单元测试类中的属性上可以使用@Autowired。比较方便。
13.2 Spring对JUnit5的支持
引入JUnit5的依赖,Spring对JUnit支持的依赖还是:spring-test,如下:
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>spring6-015-junit</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!--仓库-->
<repositories>
<!--spring里程碑版本的仓库-->
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<!--spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring对junit的支持相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--junit5依赖-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
单元测试类
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringJUnit5Test {
@Autowired
private User user;
@Test
public void testUser(){
System.out.println(user.getName());
}
}
在JUnit5当中,可以使用Spring提供的以下两个注解,标注到单元测试类上,这样在类当中就可以使用@Autowired注解了。
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:spring.xml")
14 Spring6集成MyBatis3.5
14.1 实现步骤
● 第一步:准备数据库表
○ 使用t_act表(账户表)
● 第二步:IDEA中创建一个模块,并引入依赖
○ spring-context
○ spring-jdbc
○ mysql驱动
○ mybatis
○ mybatis-spring:mybatis提供的与spring框架集成的依赖
○ 德鲁伊连接池
○ junit
● 第三步:基于三层架构实现,所以提前创建好所有的包
○ com.powernode.bank.mapper
○ com.powernode.bank.service
○ com.powernode.bank.service.impl
○ com.powernode.bank.pojo
● 第四步:编写pojo
○ Account,属性私有化,提供公开的setter getter和toString。
● 第五步:编写mapper接口
○ AccountMapper接口,定义方法
● 第六步:编写mapper配置文件
○ 在配置文件中配置命名空间,以及每一个方法对应的sql。
● 第七步:编写service接口和service接口实现类
○ AccountService
○ AccountServiceImpl
● 第八步:编写jdbc.properties配置文件
○ 数据库连接池相关信息
● 第九步:编写mybatis-config.xml配置文件
○ 该文件可以没有,大部分的配置可以转移到spring配置文件中。
○ 如果遇到mybatis相关的系统级配置,还是需要这个文件。
● 第十步:编写spring.xml配置文件
○ 组件扫描
○ 引入外部的属性文件
○ 数据源
○ SqlSessionFactoryBean配置
■ 注入mybatis核心配置文件路径
■ 指定别名包
■ 注入数据源
○ Mapper扫描配置器
■ 指定扫描的包
○ 事务管理器DataSourceTransactionManager
■ 注入数据源
○ 启用事务注解
■ 注入事务管理器
● 第十一步:编写测试程序,并添加事务,进行测试
14.2 具体实现
第一步:准备数据库表
连接数据库的工具有很多,除了之前我们使用的navicat for mysql之外,也可以使用IDEA工具自带的DataBase插件。可以根据下图提示自行配置:
第二步:IDEA中创建一个模块,并引入依赖
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>spring6-016-sm</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!--仓库-->
<repositories>
<!--spring里程碑版本的仓库-->
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.0-M2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.13</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
第三步:基于三层架构实现,所以提前创建好所有的包
第四步:编写pojo
public class Account {
private String actno;
private Double balance;
@Override
public String toString() {
return "Account{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
public Account() {
}
public Account(String actno, Double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
}
第五步:编写mapper接口
public interface AccountMapper {
/**
* 保存账户
* @param account
* @return
*/
int insert(Account account);
/**
* 根据账号删除账户
* @param actno
* @return
*/
int deleteByActno(String actno);
/**
* 修改账户
* @param account
* @return
*/
int update(Account account);
/**
* 根据账号查询账户
* @param actno
* @return
*/
Account selectByActno(String actno);
/**
* 获取所有账户
* @return
*/
List<Account> selectAll();
}
第六步:编写mapper配置文件
一定要注意,按照下图提示创建这个目录。注意是斜杠不是点儿。在resources目录下新建。并且要和Mapper接口包对应上。
如果接口叫做AccountMapper,配置文件必须是AccountMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.bank.mapper.AccountMapper">
<insert id="insert">
insert into t_act values(#{actno}, #{balance})
</insert>
<delete id="deleteByActno">
delete from t_act where actno = #{actno}
</delete>
<update id="update">
update t_act set balance = #{balance} where actno = #{actno}
</update>
<select id="selectByActno" resultType="Account">
select * from t_act where actno = #{actno}
</select>
<select id="selectAll" resultType="Account">
select * from t_act
</select>
</mapper>
● 第七步:编写service接口和service接口实现类
注意编写的service实现类纳入IoC容器管理:
AccountService接口
public interface AccountService {
/**
* 开户
* @param act
* @return
*/
int save(Account act);
/**
* 根据账号销户
* @param actno
* @return
*/
int deleteByActno(String actno);
/**
* 修改账户
* @param act
* @return
*/
int update(Account act);
/**
* 根据账号获取账户
* @param actno
* @return
*/
Account getByActno(String actno);
/**
* 获取所有账户
* @return
*/
List<Account> getAll();
/**
* 转账
* @param fromActno
* @param toActno
* @param money
*/
void transfer(String fromActno, String toActno, double money);
}
AccountServiceImpl,注入AccountMapper
package com.powernode.bank.service.impl;
import com.powernode.bank.mapper.AccountMapper;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author 动力节点
* @version 1.0
* @className AccountServiceImpl
* @since 1.0
**/
@Transactional
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public int save(Account act) {
return accountMapper.insert(act);
}
@Override
public int deleteByActno(String actno) {
return accountMapper.deleteByActno(actno);
}
@Override
public int update(Account act) {
return accountMapper.update(act);
}
@Override
public Account getByActno(String actno) {
return accountMapper.selectByActno(actno);
}
@Override
public List<Account> getAll() {
return accountMapper.selectAll();
}
@Override
public void transfer(String fromActno, String toActno, double money) {
Account fromAct = accountMapper.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new RuntimeException("余额不足");
}
Account toAct = accountMapper.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountMapper.update(fromAct);
count += accountMapper.update(toAct);
if (count != 2) {
throw new RuntimeException("转账失败");
}
}
}
第八步:编写jdbc.properties配置文件
放在类的根路径下jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring6
jdbc.username=root
jdbc.password=root
第九步:编写mybatis-config.xml配置文件
放在类的根路径下,只开启日志,其他配置到spring.xml中。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
</configuration>
第十步:编写spring.xml配置文件
<?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" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.bank"/>
<!--外部属性配置文件-->
<context:property-placeholder location="jdbc.properties"/>
<!--数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--SqlSessionFactoryBean-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<!--mybatis核心配置文件路径-->
<property name="configLocation" value="mybatis-config.xml"/>
<!--注入数据源-->
<property name="dataSource" ref="dataSource"/>
<!--起别名-->
<property name="typeAliasesPackage" value="com.powernode.bank.pojo"/>
</bean>
<!--Mapper扫描器-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.powernode.bank.mapper"/>
</bean>
<!--事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
第十一步:编写测试程序,并添加事务,进行测试
public class SMTest {
@Test
public void testSM(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
try {
accountService.transfer("act-001", "act-002", 10000.0);
System.out.println("转账成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("转账失败");
}
}
}
14.3 Spring配置文件的import
spring配置文件有多个,并且可以在spring的核心配置文件中使用import进行引入,我们可以将组件扫描单独定义到一个配置文件中,如下:
common.xml
<?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 https://www.springframework.org/schema/context/spring-context.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.bank"/>
</beans>
然后在核心配置文件中引入:spring.xml
<?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" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--引入其他的spring配置文件-->
<import resource="common.xml"/>
</beans>
注意:在实际开发中,service单独配置到一个文件中,dao单独配置到一个文件中,然后在核心配置文件中引入。