您现在的位置是:首页 >学无止境 >Spring6-02网站首页学无止境
Spring6-02
JdbcTemplate
JdbcTemplate是Spring提供的一个JDBC模板,是对JDBC的封装,简化了JDBC代码。当然也可以不用JdbcTemplate,可以让Spring集成其他的ORM框架,例如MyBatis、Hibernate等。接下来使用JdbcTemplate完成增删改查。
环境准备
-
数据库表:t_user
-
IDEA中新建模块:spring6-007-jdbc
-
引入相关依赖
<?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。
package com.powernode.spring6.bean; /** * @author 动力节点 * @version 1.0 * @className User * @since 1.0 **/ 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; } }
-
编写Spring配置文件:
JdbcTemplate是Spring提供好的类,这类的完整类名是:org.springframework.jdbc.core.JdbcTemplate
-
我们怎么使用这个类呢?new对象就可以了。怎么new对象,Spring最在行了。直接将这个类配置到Spring配置文件中,纳入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="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"></bean> </beans>
-
我们来看一下这个JdbcTemplate源码:
-
可以看到JdbcTemplate中有一个DataSource属性,这个属性是数据源,我们都知道连接数据库需要Connection对象,而生成Connection对象是数据源负责的,所以我们需要给JdbcTemplate设置数据源属性。
-
在JdbcTemplate对象中唯一必须提供的属性就是dateSource属性。
-
所有的数据源都是要实现javax.sql.DataSource接口的。这个数据源可以自己写一个,也可以用写好的,不如阿里巴巴的德鲁伊连接池,c3p0,dbcp等。我们这里自己先手写一个数据源。
package com.powernode.spring6.jdbc; import javax.sql.DataSource; import java.io.PrintWriter; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.logging.Logger; /** * @author 动力节点 * @version 1.0 * @className MyDataSource * @since 1.0 **/ 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。因为JdbcTemplate对象中有一个必须的属性就是dataSource属性。
<?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>
新增
package com.powernode.spring6.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* @author 动力节点
* @version 1.0
* @className JdbcTest
* @since 1.0
**/
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);
}
}
update方法有两个参数:
- 第一个参数:要执行的SQL语句。(SQL语句中可能会有占位符)
- 第二个参数:可变长参数,参数的个数可以是0个,也可以是多个。一般是SQL语句中有几个问号,则对应几个参数。
修改
@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);
}
执行结果:
删除
@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);
}
执行结果:
查一个对象
@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方法有三个参数
- 第一个参数:要执行的DQL sql语句
- 第二个参数:Bean属性值和数据库记录行的映射对象【BeanPropertyRowMapper】。在构造方法中指定映射的对象类型。
- 第三个参数:可变长参数,给sql语句的占位符“?”传值。
查多个对象
@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);
}
执行结果:
查询一个值
@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);
}
执行结果:
批量添加
@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数组底层对应执行一条sql语句
// SQL语句中有几个占位符“?”一个Object数组中就有几个元素
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));
}
执行结果:
batchUpdate方法有两个参数:
- 第一个参数:要执行的sql语句
- 第二个参数:一个给占位符传值的List<Object[]> 集合。
批量修改
@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));
}
运行结果:
批量删除
@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));
}
运行结果:
使用回调函数
@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);
}
执行结果:
- 优点:可以参化的更加细节。
- PreparedStatementCallback类中写JDBC代码。
- execute方法,有两个参数
- 第一个参数:要执行sql语句
- 第二个参数:PreparedStatementCallback类对象
使用德鲁伊连接池
-
之前数据源是用我们自己写的。也可以使用别人写好的。例如比较牛的德鲁伊连接池。
-
第一步:引入德鲁伊连接池的依赖。(毕竟是别人写的)
<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>
-
测试结果:
总结
- 使用JdbcTemplate增上改的时候:
- 对单条记录进行增删改的时候使用“jdbcTemplate.update(sql,args…)”
- 批量操作增删改的时候使用:“jdbcTemplate.batchUpdate(sql,List<Objext[]>)”
- 使用JdbcTemplate查询的时候:
- 查询单条记录:“jdbcTemplate.queryForObject(sql,new BeanPropertyRowMapper(类名.class),args…)”
- 查询所有记录:“jdbcTemplate.query(sql,new BeanPropertyRowMapper(类名.class))”
- 使用JdbcTemplate查询单个值的时候
- 使用“jdbcTemplate.queryForObject(sql,查询的这个值的类型.class)”
GoF之代理模式
对代理模式的理解
-
什么时候需要使用代理?
- 在程序中A对象无法和B对象直接进行交互的时候。
- 在程序中功能需要增强的时候。
- 在程序中目标对象需要被保护时。
-
代理模式中的角色:
- 目标对象(其中有目标方法)
- 代理对象(其中有代理方法)
- 目标类和代理类公共接口:客户端在使用代理类的时候就像在使用目标类,不被客户端锁察觉,所以代理类和目标类要有共同的行为,也就是实现共同的接口。
-
代理模式的类图:
-
代理模式在代码上包括两种形式:
- 静态代理
- 动态代理
静态代理
现在有这样一个接口和实现类:
package com.powernode.mall.service;
/**
* 订单接口
* @author 动力节点
* @version 1.0
* @className OrderService
* @since 1.0
**/
public interface OrderService {
/**
* 生成订单
*/
void generate();
/**
* 查看订单详情
*/
void detail();
/**
* 修改订单
*/
void modify();
}
package com.powernode.mall.service.impl;
import com.powernode.mall.service.OrderService;
/**
* @author 动力节点
* @version 1.0
* @className OrderServiceImpl
* @since 1.0
**/
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("订单已修改");
}
}
其中Thread.sleep()方法的调用为了模拟网络延迟。
项目已上线,并且运行正常,只是客户反馈系统有一些地方运行较慢,要求项目组对系统进行优化。于是项目负责人就下达了这个需求。首先需要搞清楚是哪些业务方法耗时较长,于是让我们统计每个业务方法所耗费的时长。如果是你,你该怎么做呢?
第一种方案:直接修改Java源代码,在每个业务方法中添加统计逻辑,如下:
package com.powernode.mall.service.impl;
import com.powernode.mall.service.OrderService;
/**
* @author 动力节点
* @version 1.0
* @className OrderServiceImpl
* @since 1.0
**/
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
long begin = System.currentTimeMillis();
try {
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成");
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
@Override
public void detail() {
long begin = System.currentTimeMillis();
try {
Thread.sleep(2541);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单信息如下:******");
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
@Override
public void modify() {
long begin = System.currentTimeMillis();
try {
Thread.sleep(1010);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改");
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
}
需求可以满足,但是违背了OCP开闭原则,这种方案不可取。
第二种方案:编写一个子类集成OrderServiceImpl,在子类中重写每个方法,代码如下:
package com.powernode.mall.service.impl;
/**
* @author 动力节点
* @version 1.0
* @className OrderServiceImplSub
* @since 1.0
**/
public class OrderServiceImplSub extends OrderServiceImpl{
@Override
public void generate() {
long begin = System.currentTimeMillis();
super.generate();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
@Override
public void detail() {
long begin = System.currentTimeMillis();
super.detail();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
@Override
public void modify() {
long begin = System.currentTimeMillis();
super.modify();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
}
这种方式可以解决,但是存在两个问题:
- 第一个问题:假设系统中有100个这样的业务类,需要提供100个子类,并且之前写好的创建Service对象的代码,都要修改为创建子类对象。
- 第二个问题:由于采用了继承的方式,导致代码之间的耦合度较高。
这种方式不可取。
第三种方案:使用代理模式(这里采用静态代理)
可以为OrderService接口提供一个代理类。
package com.powernode.mall.service;
/**
* @author 动力节点
* @version 1.0
* @className OrderServiceProxy
* @since 1.0
**/
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)+"毫秒");
}
}
这种方式的优点:符合OCP开闭原则,同时采用的是关联关系,所以程序的耦合度较低。所以这种方案是被推荐的。【关联关系的耦合度低于继承关系】
编写客户端程序:
package com.powernode.mall;
import com.powernode.mall.service.OrderService;
import com.powernode.mall.service.OrderServiceProxy;
import com.powernode.mall.service.impl.OrderServiceImpl;
/**
* @author 动力节点
* @version 1.0
* @className Client
* @since 1.0
**/
public class Client {
public static void main(String[] args) {
// 创建目标对象
OrderService target = new OrderServiceImpl();
// 创建代理对象
OrderService proxy = new OrderServiceProxy(target);
// 调用代理对象的代理方法
proxy.generate();
proxy.modify();
proxy.detail();
}
}
运行结果:
以上就是代理模式中的静态代理,其中OrderService接口是代理类和目标类的共同接口。OrderServiceImpl是目标类。OrderServiceProxy是代理类。
思考:如果系统中业务接口很多,一个接口对应一个代理类,显然也不合理,会导致类爆炸。怎么解决这个问题?动态代理可以解决,因为动态代理中可以在内存中动态为我们生成代理类的字节码。代理类不需要我们写了。类爆炸解决了,而且代码只需要写一次,代码也得到了复用。
动态代理
- 在程序运行阶段,在内存中动态生成代理类,被称为动态代理。目的是减少代理类的数量,解决代码复用问题。
- 在内存当中动态生成类的技术常见的包括:
- JDK动态代理技术:只能代理接口。
- CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比JDK动态代理要好。(底层有一个小而快的字节码处理框架ASM。)
- Javassist动态代理技术:Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。
JDK动态代理
-
OrderService接口
package com.powernode.mall.service; /** * 订单接口 * @author 动力节点 * @version 1.0 * @className OrderService * @since 1.0 **/ public interface OrderService { /** * 生成订单 */ void generate(); /** * 查看订单详情 */ void detail(); /** * 修改订单 */ void modify(); }
-
OrderService接口实现类
package com.powernode.mall.service.impl; import com.powernode.mall.service.OrderService; /** * @author 动力节点 * @version 1.0 * @className OrderServiceImpl * @since 1.0 **/ 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("订单已修改"); } }
-
我们在静态代理的时候,除了以上一个接口和一个实现类之外,是不是要写一个代理类UserServiceProxy呀!在动态代理中UserServiceProxy代理类是可以动态生成的。这个类不需要写。我们直接写客户端程序即可:
package com.powernode.mall; import com.powernode.mall.service.OrderService; import com.powernode.mall.service.impl.OrderServiceImpl; import java.lang.reflect.Proxy; /** * @author 动力节点 * @version 1.0 * @className Client * @since 1.0 **/ public class Client { public static void main(String[] args) { // 第一步:创建目标对象 OrderService target = new OrderServiceImpl(); // 第二步:创建代理对象 OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), 调用处理器对象); // 第三步:调用代理对象的代理方法 orderServiceProxy.detail(); orderServiceProxy.modify(); orderServiceProxy.generate(); } }
-
以上第二步创建代理对象是需要大家理解的:
OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), 调用处理器对象);
这行代码做了两件事:
- 第一件事:在内存中生成了代理类的字节码
- 第二件事:创建代理对象
-
Proxy类全名:java.lang.reflect.Proxy。这是JDK提供的一个类(所以称为JDK动态代理)。主要是通过这个类在内存中生成代理类的字节码。
-
其中newProxyInstance()方法有三个参数:
- 第一个参数:类加载器。要执行使用哪个类的类加载器。一般是目标类对象的类加载器。
- 第二个参数:接口类型。代理类和目标类实现相同的接口,所以要通过这个参数告诉JDK动态代理生成的类要实现那些接口。
- 第三个参数:调用处理器。这是一个JDK动态代理规定的接口,接口全名:java.lang.reflect.InvocationHandler。显然这是一个回调接口,也就是说调用这个接口中方法的程序已经写好了,就差这个接口的实现类了。
-
-
所以接下来我们要写一下java.lang.reflect.InvocationHandler接口的实现类,并且实现接口中的方法,代码如下:
package com.powernode.mall.service; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * @author 动力节点 * @version 1.0 * @className TimerInvocationHandler * @since 1.0 **/ public class TimerInvocationHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return null; } }
-
InvocationHandler接口中共有一个方法invoke方法。这个方法有三个参数:
- 第一个参数:Object proxy。代理对象。设计这个参数只是为了后期的方便,如果想在invoke方法中使用代理对象的话,尽管通过这个参数来使用。
- 第二个参数:Method method。目标方法。
- 第三个参数:Object[] args。目标方法调用时要传的参数。
-
我们将来肯定是要调用“目标方法”的,但要调用目标方法的话,需要“目标对象”的存在,“目标对象”从哪儿来呢?我们可以给TimerInvocationHandler提供一个构造方法,可以通过这个构造方法传过来“目标对象”,代码如下:
package com.powernode.mall.service; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * @author 动力节点 * @version 1.0 * @className TimerInvocationHandler * @since 1.0 **/ 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 { return null; } }
-
有了目标对象我们就可以在invoke()方法中调用目标方法了。代码如下:
package com.powernode.mall.service; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * @author 动力节点 * @version 1.0 * @className TimerInvocationHandler * @since 1.0 **/ 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; } }
-
到此为止,调用处理器就完成了。接下来,应该继续完善Client程序:
package com.powernode.mall; import com.powernode.mall.service.OrderService; import com.powernode.mall.service.TimerInvocationHandler; import com.powernode.mall.service.impl.OrderServiceImpl; import java.lang.reflect.Proxy; /** * @author 动力节点 * @version 1.0 * @className Client * @since 1.0 **/ 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(); } }
-
大家可能会比较好奇:那个InvocationHandler接口中的invoke()方法没看见在哪里调用呀?
当你调用代理对象的代理方法的时候,注册在InvocationHandler接口中的invoke()方法会被调用。也就是上面代码第24 25 26行,这三行代码中任意一行代码执行,注册在InvocationHandler接口中的invoke()方法都会被调用。
-
学到这里可能会感觉有点懵,折腾半天,到最后这不是还得写一个接口的实现类吗?没省劲儿呀?
-
你要这样想就错了!!!
-
我们可以看到,不管你有多少个Service接口,多少个业务类,这个TimerInvocationHandler接口是不是只需要写一次就行了,代码是不是得到复用了!!!!
而且最重要的是,以后程序员只需要关注核心业务的编写了,像这种统计时间的代码根本不需要关注。因为这种统计时间的代码只需要在调用处理器中编写一次即可。
CGLIB动态代理
面向切面编程AOP
IoC使软件组件松耦合。AOP让你能够捕捉系统中经常使用的功能,把他装化成组件。
AOP(Aspect Oriented Programming):面向切面编程,面向方面编程。(AOP是一种编程技术(是一种编程思想))
AOP是对OOP的补充和延申。
AOP底层用的就是动态代理实现的。
Spring的AOP使用的动态代理是:JDK动态代理+CJLIB动态代理。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。当然,你也可以强制通过一些配置让Spring只使用CGLIB。
AOP的介绍
-
一般一个系统当中都会有一些系统服务,例如:日志、事务处理、安全等。这些系统服务称为交叉业务。
-
这些交叉业务几乎都是通用的,不管是做银行账户转账还是删除用户数据。日志、事务管理、安全这些都需要做。
-
如果在每一个业务处理过程当中,都掺杂着这些交叉业务代码进去的话,存在两方面问题:
- 交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处。
- 程序员无法专注核心业务代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务。
-
使用AOP可以轻松解决上述问题。
-
请看下图,可以帮助快速理解AOP的思想。
-
总结AOP:将与核心业务无关的代码独立抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP。
-
AOP的优点:
- 第一:代码复用性强。
- 第二:代码易于维护。
- 第三:是开发者更关注业务逻辑。
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:被植入通知的对象。
-
通过下图,大家可以很好的理解AOP的相关术语:
切点表达式
-
切点表达式是用来定义通知(Advice)往那些方法上切入。
-
切点表达式的语法格式:
execution([访问控制权限修饰符] 返回值类型 [全限定类名].方法名(形式参数列表) [异常])
- 访问控制权限修饰符
- 可选项。
- 没写,就是4个权限都包括。
- 写public就表示只包括公开的方法。
- 返回值类型:
- 必填项。
- “ * ”表示返回值类型任意。
- 全限定类名
- 可选项。
- 两个点“…”代表当前包以及子包下的所有类。
- 省略时表示所有的类。
- 方法名
- 必填项。
- “ * ” 表示所有方法。
- set*:表示所有set方法。
- 形式参数列表
- 必填项。
- ()表示没有参数的方法。
- (…)表示参数类型和个数随意的方法
- (*,String)表示第一个参数随意,第二个参数是String类型的。
- 异常
- 可选项。
- 省略时,表示任意异常类型。
- 访问控制权限修饰符
-
理解一下切点表达式:
execution(public * com.powernode.mall.service.*.delete*(..)); //表示service包下所有的类中以delete开始的所有方法
execution(* com.powernode.mall..*(..)); //mall包下所有的类的所有方法
execution(* *(..)); //表示所有的类的所有方法
使用Spring的AOP
-
Spring对AOP的实现包括以下三种方式:【前两中重要,第三种几乎不用】
- 第一种:Spring结合AspectJ框架实现的AOP,基于注解方式。
- 第二种:Spring结合AspectJ框架实现的AOP,基于XML注解方式。
- 第三种:Spring框架自己实现的AOP,基于XML配置方式。
-
什么是AspectJ?
-
AspectJ是一个面向切面编程(AOP)的框架,它扩展了Java语言,提供了用于在运行时动态植入代码的能力。使用AspectJ,开发人员可以在不修改源代码的情况下,通过创建切面来实现系统范围内的功能,如日志记录、性能监控、事务管理等。
AspectJ支持很多种类型的切面,例如前置通知、后置通知、环绕通知、异常通知和引入通知等。这些通知可以被组合成切面,以便在应用程序的多个地方重复使用。
-
准备工作
-
使用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>
-
Spring配置文件中添加context命名空间和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: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"> </beans>
基于AspectJ的AOP注解式开发
实现步骤
-
定义目标类以及目标方法。
package com.powernode.spring6.service; // 目标类 public class OrderService { // 目标方法 public void generate(){ System.out.println("订单已生成!"); } }
-
第二步:定义切面类
package com.powernode.spring6.service; import org.aspectj.lang.annotation.Aspect; // 切面类 @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>
-
第五步:在切面类中添加通知
package com.powernode.spring6.service; import org.springframework.stereotype.Component; import org.aspectj.lang.annotation.Aspect; // 切面类 @Aspect @Component public class MyAspect { // 这就是需要增强的代码(通知) public void advice(){ System.out.println("我是一个通知"); } }
-
第六步:在通知上添加切点表达式。
package com.powernode.spring6.service; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import org.aspectj.lang.annotation.Aspect; // 切面类 @Aspect @Component public class MyAspect { // 切点表达式 @Before("execution(* com.powernode.spring6.service.OrderService.*(..))") // 这就是需要增强的代码(通知) public void advice(){ System.out.println("我是一个通知"); } }
-
注解@Before表示前置通知。
<?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"></aop:aspectj-autoproxy>
- 这行代码作用:
- 有了这行代码:Spring容器扫描应用程序上下文所有的Bean定义,找出需要被代理的Bean。
- 对于每一个需要被代理的Bean,Spring都会检查是否有与之关联的切面定义,如果有就会为这个需要被代理的bean创建一个代理对象,并使用切面对该对象进行增强。
- 使用Spring再去调用原来的对象的时候,就会调用道这个新的代理对象上来。
- proxy-target-class=“true”:表示采用cglib动态代理。【语义:代理目标类,而不是目标接口】
- proxy-target-class=“false” 表示采用jdk动态代理。默认值是false。即使写成false,当没有接口的时候,也会自动选择cglib生成代理类。
- 这行代码作用:
-
-
测试程序:
package com.powernode.spring6.test; import com.powernode.spring6.service.OrderService; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AOPTest { @Test public void testAOP(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aspectj-aop-annotation.xml"); OrderService orderService = applicationContext.getBean("orderService", OrderService.class); orderService.generate(); } }
-
运行结果:
通知类型
-
通知的类型包括:
- 前置通知:@Before目标方法执行之前的通知
- 后置通知:@AfterReturning目标方法执行之后的通知
- 环绕通知:@Around目标方法执行之前通知,同时目标方法执行之后添加通知。
- 异常通知:@AfterThrowing目标方法执行发生异常之后执行的通知
- 最终通知:@After,放在finally语句块中的通知
-
编写代码测试
package com.powernode.spring6.service; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; // 切面类 @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("最终通知"); } }
- 在Spring中,
ProceedingJoinPoint
是一个接口,它提供了访问目标方法和参数的能力.[通过调用ProceedingJoinPoint对象的proceed方法可以执行目标方法] - 每个类型的通知可以都有一个类型的参数。【JoinPoint类型的参数,可以通过JoinPoint类型的对象获取方法的签名 】,但是只有环绕通知可以有“ProceedingJoinPoint”类型的参数。
- 在Spring中,
-
目标类和目标方法
package com.powernode.spring6.service; import org.springframework.stereotype.Component; // 目标类 @Component public class OrderService { // 目标方法 public void generate(){ System.out.println("订单已生成!"); } }
-
测试程序
package com.powernode.spring6.test; import com.powernode.spring6.service.OrderService; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AOPTest { @Test public void testAOP(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aspectj-aop-annotation.xml"); OrderService orderService = applicationContext.getBean("orderService", OrderService.class); orderService.generate(); } }
-
执行结果:
- 通过上面执行结果可以判断出他们的执行顺序。
-
结果中没有异常通知,这是因为目标程序执行过程中没有发生异常。我们尝试让目标方法发生异常:
package com.powernode.spring6.service; import org.springframework.stereotype.Component; // 目标类 @Component public class OrderService { // 目标方法 public void generate(){ System.out.println("订单已生成!"); if (1 == 1) { throw new RuntimeException("模拟异常发生"); } } }
-
再次执行程序:
通过测试得知,当异常发生后,最终通知也会执行,因为最终通知@After会出现在finally语句块中。【出现异常之后后置通知和环绕后置通知不在执行】
切面执行的先后顺序
-
我们知道,业务流程当中不一定只有一个切面,可能有的切面控制事务,有的切面控制日志,有的切面进行安全控制,如果多个切面的话,顺序应该如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小优先级越高
-
在定义一个切面类如下:
package com.powernode.spring6.service; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @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最终通知"); } }
-
设置切面类的优先级
package com.powernode.spring6.service; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; // 切面类 @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("最终通知"); } }
-
执行测试程序:
优化使用切点表达式
-
观看以下代码中的切点表达式:
package com.powernode.spring6.service; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; // 切面类 @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("最终通知"); } }
- 缺点:
- 第一:切点表达式重复写了多次,没有得到复用。
- 第二:如果要修改切点表达式需要修改多处,难维护。
- 缺点:
-
可以这样做:将切点表达式单独的定义出来,在需要的位置引入即可。如下:
package com.powernode.spring6.service; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; // 切面类 @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("最终通知"); } }
-
注意这个@Pointcut注解标注的方法随意,可以在任意方法上编写,不干扰到某个方法的正常执行,在使用切点表达式的时候用这个方法名来标识这个切点。
-
在一个类中使用@Pointcut定义的切点,在另一个类中需要使用的时候,需要加上全限定类名“全限定类名.方法名()”
//定义 @Pointcut("execution(切点表达式)") public void pointcut() {} //使用 @Before("pointcut()") public void xxXX(){ .... }
-
全注解式开发
就是编写一个类,在这个类上面使用大量注解来代替spring的配置文件,spring配置文件消失了,如下
package com.powernode.spring6.service;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@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();
}
执行结果:
基于XML配置方式的AOP
-
编写目标类:
package com.powernode.spring6.service; // 目标类 public class VipService { public void add(){ System.out.println("保存vip信息。"); } }
-
编写切面类,并且编写通知
package com.powernode.spring6.service; import org.aspectj.lang.ProceedingJoinPoint; // 负责计时的切面类 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"> <!--切面=通知 + 切点--> <!--通知时time方法,切点表达式是“p”--> <aop:around method="time" pointcut-ref="p"/> </aop:aspect> </aop:config> </beans>
-
测试程序
package com.powernode.spring6.test; import com.powernode.spring6.service.VipService; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AOPTest3 { @Test public void testAOPXml(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aop-xml.xml"); VipService vipService = applicationContext.getBean("vipService", VipService.class); vipService.add(); } }
-
执行结果
AOP的实际案例:事务处理
-
项目中的事务控制是在所难免的。在一个业务流程当中,可能需要多条DML语句共同完成,为了保证数据的安全,这多条DML语句要么同时成功,要么同时失败。这就需要添加事务控制的代码。例如以下伪代码:
class 业务类1{ public void 业务方法1(){ try{ // 开启事务 startTransaction(); // 执行核心业务逻辑 step1(); step2(); step3(); .... // 提交事务 commitTransaction(); }catch(Exception e){ // 回滚事务 rollbackTransaction(); } } public void 业务方法2(){ try{ // 开启事务 startTransaction(); // 执行核心业务逻辑 step1(); step2(); step3(); .... // 提交事务 commitTransaction(); }catch(Exception e){ // 回滚事务 rollbackTransaction(); } } public void 业务方法3(){ try{ // 开启事务 startTransaction(); // 执行核心业务逻辑 step1(); step2(); step3(); .... // 提交事务 commitTransaction(); }catch(Exception e){ // 回滚事务 rollbackTransaction(); } } } class 业务类2{ public void 业务方法1(){ try{ // 开启事务 startTransaction(); // 执行核心业务逻辑 step1(); step2(); step3(); .... // 提交事务 commitTransaction(); }catch(Exception e){ // 回滚事务 rollbackTransaction(); } } public void 业务方法2(){ try{ // 开启事务 startTransaction(); // 执行核心业务逻辑 step1(); step2(); step3(); .... // 提交事务 commitTransaction(); }catch(Exception e){ // 回滚事务 rollbackTransaction(); } } public void 业务方法3(){ try{ // 开启事务 startTransaction(); // 执行核心业务逻辑 step1(); step2(); step3(); .... // 提交事务 commitTransaction(); }catch(Exception e){ // 回滚事务 rollbackTransaction(); } } } //......
-
可以看到,这些业务类中的每一个业务方法都是需要控制事务的,而控制事务的代码又是固定的格式,都是:
try{ // 开启事务 startTransaction(); // 执行核心业务逻辑 //...... // 提交事务 commitTransaction(); }catch(Exception e){ // 回滚事务 rollbackTransaction(); }
-
这个控制事务的代码就是和业务逻辑没有关系的“交叉业务”。以上伪代码当中可以看到这些交叉业务的代码没有得到复用,并且如果这些交叉业务代码需要修改,那必然需要修改多处,难维护,怎么解决?可以采用AOP思想解决。可以把以上控制事务的代码作为环绕通知,切入到目标类的方法当中。接下来我们做一下这件事,有两个业务类,如下:
-
银行账户的业务类:
package com.powernode.spring6.biz; import org.springframework.stereotype.Component; @Component // 业务类 public class AccountService { // 转账业务方法 public void transfer(){ System.out.println("正在进行银行账户转账"); } // 取款业务方法 public void withdraw(){ System.out.println("正在进行取款操作"); } }
-
订单业务类
package com.powernode.spring6.biz; import org.springframework.stereotype.Component; @Component // 业务类 public class OrderService { // 生成订单 public void generate(){ System.out.println("正在生成订单"); } // 取消订单 public void cancel(){ System.out.println("正在取消订单"); } }
-
注意,以上两个业务类已经纳入spring bean的管理,因为都添加了@Component注解。
-
接下来我们给以上两个业务类的4个方法添加事务控制代码,使用AOP来完成:
package com.powernode.spring6.biz; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @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("回滚事务"); } } }
-
这个事务控制代码是不是只需要写一次就行了,并且修改起来也没有成本。编写测试程序:
package com.powernode.spring6.test; import com.powernode.spring6.biz.AccountService; import com.powernode.spring6.biz.OrderService; import com.powernode.spring6.service.Spring6Configuration; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; 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(); } }
-
执行结果:
通过测试可以看到,所有的业务方法都添加了事务控制的代码。
AOP的实际案例:安全日志
-
需求是这样的:项目开发结束了,已经上线了。运行正常。客户提出了新的需求:凡事在系统中进行修改操作的,删除操作的,新增操作的,都要把这个人记录下来。因为这几个操作是属于危险行为。例如有业务类和业务方法:
-
用户业务类
package com.powernode.spring6.biz; import org.springframework.stereotype.Component; @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("修改用户"); } }
-
商品业务类:
package com.powernode.spring6.biz; import org.springframework.stereotype.Component; // 商品业务类 @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来解决上面的需求:编写一个负责安全的切面类
package com.powernode.spring6.biz; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @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(); }
-
执行结果:
Spring对事务的支持
事务的概述
- 什么是事务:
- 在一个业务流程当中,通常要有多条DML语句(insert、delete、update)语句共同联合才能完成,这么多条DML语句必须同时成功,或者同时失败,这样才能保证数据的安全。
- 多条DML语句要么同时成功,要么同时失败,这叫做事务。
- 事务:Transaction(tx)
- 事务的四个处理过程:
- 第一步:开启事务(start transaction)。
- 第二步:执行核心业务逻辑。
- 第三步:提交事务(如果核心业务处理过程中没有异常出现)(commit transaction)
- 第四步:回滚事务(如果核心业务处理过程中出现异常)(rollback transaction)
- 事务的四个特性:
- 原子性:事务是最小的工作单元,不可再分。
- 一致性:事务要求要么同时成功,要么同时失败。
- 隔离性:事务和事务之间因为具有隔离性,才互不干扰。
- 持久性:持久性,是事务结束的标志。将数据持久化到硬盘数据库文件中这样事务就结束了。
引入事务的场景
-
以银行账户转账为例学习事务。两个账户act-001和act-002。act-001账户向act-002账户转账10000,必须同时成功,或者同时失败。(一个减成功,一个加成功, 这两条update语句必须同时成功,或同时失败。)
-
连接数据库的技术采用Spring框架的JdbcTemplate。
-
采用三层架构搭建:
-
模块名:spring6-013-tx-bank(依赖如下)
<?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
package com.powernode.bank.pojo;
/**
* @author 动力节点
* @version 1.0
* @className Account
* @since 1.0
**/
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;
}
}
第四步:编写持久层
package com.powernode.bank.dao;
import com.powernode.bank.pojo.Account;
/**
* @author 动力节点
* @version 1.0
* @className AccountDao
* @since 1.0
**/
public interface AccountDao {
/**
* 根据账号查询余额
* @param actno
* @return
*/
Account selectByActno(String actno);
/**
* 更新账户
* @param act
* @return
*/
int update(Account act);
}
package com.powernode.bank.dao.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
/**
* @author 动力节点
* @version 1.0
* @className AccountDaoImpl
* @since 1.0
**/
@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;
}
}
第五步:编写业务层
package com.powernode.bank.service;
/**
* @author 动力节点
* @version 1.0
* @className AccountService
* @since 1.0
**/
public interface AccountService {
/**
* 转账
* @param fromActno
* @param toActno
* @param money
*/
void transfer(String fromActno, String toActno, double money);
}
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* @author 动力节点
* @version 1.0
* @className AccountServiceImpl
* @since 1.0
**/
@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>
第七步:编写表示层(测试程序)
package com.powernode.spring6.test;
import com.powernode.bank.service.AccountService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author 动力节点
* @version 1.0
* @className BankTest
* @since 1.0
**/
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();
}
}
}
执行结果:
数据变化:
模拟异常
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* @author 动力节点
* @version 1.0
* @className AccountServiceImpl
* @since 1.0
**/
@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("转账失败,请联系银行");
}
}
}
执行结果:
数据库表中数据:
丢了一万块钱
Spring对事务的支持
Spring实现事务的两种方式
-
编程式事务
- 通过编写代码的方式来实现事务的管理
-
声明式事务
- 基于注解方式
- 基于XML方式
-
声明式事务,为什么叫做“声明式”?
-
声明式事务是指使用特定的语法和标记来表示一个事务的边界和属性,而不需要显式地编写代码来执行该事务。这种方式可以让开发者更加专注于业务逻辑的实现,而不必过多关注事务管理的细节。
之所以称为“声明式”,是因为在编写代码时,开发者只需要声明事务的一些基本属性,如隔离级别、传播行为、超时时间等,而不需要手动编写代码来实现具体的事务逻辑。这种声明式的方式更加直观、简洁,能够大大提高开发效率和代码质量,并且减少了出错的概率。
-
Spring事务管理api
-
Spring对事务的管理底层实现方式是基于aop实现的,采用aop方式进行了封装。所以Spring专门针对事务开发了一套API,API的核心接口如下:
- PlatformTransactionManager接口:Spring事务管理器的核心接口。在Spring6中他有两个实现:
- DataSourceTransactionManager:支持JdbcTemplate、MyBatis、Hibernate事务管理。
- JtaTransactionManager:支持分布式事务管理。
- PlatformTransactionManager接口:Spring事务管理器的核心接口。在Spring6中他有两个实现:
-
如果要在Spring6中使用JdbcTemplate,就要使用DataSourceTransactionManager来管理事务。(Spring内置写好了,可以直接用。)
声明式事务(注解实现)
-
第一步:在spring配置文件中配置事务管理器。
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!--因为使用的式JdbcTemplate来操作数据库的,所以使用Spring帮我们实现的DataSourceTransactionManager类来完成事务的管理,如果式分布式事务管理,那么就需要使用JtaTransactionManager来管理事务-->
- 因为事务的管理式需要通过连接对象的,而数据源可以提供连接对象,数据源又叫做数据库连接池,DataSourceTransactionManager对象有一个必要的属性dataSource属性。
-
第二步:在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注解。
在类上添加注解,该类中所有方法都有事务。在某个方法上添加注解,标识只有这个方法上使用注解。
package com.powernode.bank.service.impl; import com.powernode.bank.dao.AccountDao; import com.powernode.bank.pojo.Account; import com.powernode.bank.service.AccountService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * @author 动力节点 * @version 1.0 * @className AccountServiceImpl * @since 1.0 **/ @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("转账失败,请联系银行"); } } }
-
执行测试程序:
发生了异常但是数据库中的数据式安全的。
事务属性
事务包括那些属性?
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
String[] label() default {};
Propagation propagation() default Propagation.REQUIRED;//事务的传播行为
Isolation isolation() default Isolation.DEFAULT;//事务的隔离级别
int timeout() default -1;//事务的超时时间,-1表示超时时间无限长,没有超时时间。
String timeoutString() default "";
boolean readOnly() default false;//设置事务的只读性
Class<? extends Throwable>[] rollbackFor() default {};//设置表示遇到什么样的异常回滚事务
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};//设置遇到什么样的异常不回滚事务
String[] noRollbackForClassName() default {};
}
事物的传播行为
-
什么是事务的传播行为?
- 在Spring框架中,事务的传播行为是指当一个方法调用另外一个方法时,如何处理这些方法之间共享的事务。具体来说,它描述了在多个事务操作嵌套的情况下,每个操作应该如何参与现有的事务。
- 在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。
-
事务传播行为在spring框架中被定义为枚举类型:
public enum Propagation { REQUIRED(0),//“必须的”,支持当前事务,如果不存在,则新建一个事务。【有就加入,没有就新建】 SUPPORTS(1),//“支持”,支持当前事务,如果当前有事务就加入当前事务,如果当前没有事务,就以非实物方式执行。【有就加入,没有就不管了】 MANDATORY(2),//“强制性的”,必须运行在一个事务当中,如果当前没有事务正在发生,将抛出一个异常。【有就加入,没有就抛异常】 REQUIRES_NEW(3),//开启一个新的事务,如果事务已经存在,则将这个存在的事务挂起。【不管有没有,直接开启一个新事物,开启的新事物和之前的事务不存在嵌套关系,之前的事务被挂起】 NOT_SUPPORTED(4),//以非事务的方式运行,如果存在事务,挂起当前事务。【不支持事务,存在就挂起,以非实物的方式运行】 NEVER(5),//以非实物的方式运行,如果有事务存在抛出异常【不支持事务,存在就抛出异常】 NESTED(6);//如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。 private final int value; private Propagation(int value) { this.value = value; } public int value() { return this.value; } }
- REQUIRES_NEW和NESTED都是事务的传播行为,这两种行为都会开启一个新的事务,这两种事务传播行为有什么区别?
- REQUIRES_NEW 会挂起当前事务(如果存在),并创建一个新的事务。这意味着即使外部事务失败,新的事务也会提交。REQUIRES_NEW 可以被认为是一种非常独立的事务,它完全隔离于外部事务。
- NESTED 则是在当前事务的上下文中嵌套一个新的事务。嵌套的事务可以看作是当前事务的子事务,它们共享一个物理事务,并且只有当外部事务提交时,才会将更改保存到数据库中。如果嵌套的事务失败,那么整个事务(包括外部事务和所有嵌套的事务)都会被回滚。需要注意的是,NESTED 只能在支持保存点的数据库上使用。
- 因此,REQUIRES_NEW 和 NESTED 的主要区别在于事务之间的隔离级别和对事务的提交和回滚的控制方式。REQUIRES_NEW 提供了更强的隔离性和独立性,但可能会导致不一致的结果。而 NESTED 则提供了更好的一致性,但在某些情况下可能会导致性能问题。
- REQUIRES_NEW和NESTED都是事务的传播行为,这两种行为都会开启一个新的事务,这两种事务传播行为有什么区别?
-
在代码中设置事务的传播行为:
@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语句。 }
事务的隔离级别
-
什么是事务的隔离级别?
- 事务的隔离级别是指多个事务同时运行时,每个事务所能看到的数据状态以及并发操作时可能出现的问题的程度。
- 事务隔离级别类似于教室A和教室B之间的那道墙,隔离级别越高表示墙体越厚。隔音效果越好。隔离级别也就越高。
-
数据库中读取数据存在的三大问题:
- 脏读:读取到没有提交的数据,叫做脏读。
- 不可重复读:在同一个事务当中,每次读取到的数据不一样。
- 幻读:读到的数据是假的。【只要存在多线程并发就一定存在幻读问题,只有序列化的隔离级别没有幻读问题】
-
事务的四个隔离级别:
- 读未提交:READ_UNCOMMITTED
- 这种隔离级别存在脏读问题,所谓的脏读表示能够读取到其他事务未提交的数据。
- 读已提交:READ_COMMITTED
- 解决了脏读问题,表示其他事物提交之后才能读到,但是存在不可重复读的问题。
- 可重复读:REPEATABLE_READ
- 解决了不可重复读的问题,可以达到可重复读的效果,只要当前事务不结束,在当前事务中读取到的数据都是一样的,但是存在幻读的问题。
- 序列化:SERIALIZABLE
- 解决了幻读问题,事务排队执行,不支持并发。
在Spring如何设置事务的隔离级别?
隔离级别在spring中以枚举类型存在:
public enum Isolation { DEFAULT(-1), READ_UNCOMMITTED(1), READ_COMMITTED(2), REPEATABLE_READ(4), SERIALIZABLE(8); private final int value; private Isolation(int value) { this.value = value; } public int value() { return this.value; } }
@Transactional(isolation = Isolation.READ_COMMITTED)
测试事务隔离级别:READ_UNCOMMITTED 和 READ_COMMITTED
怎么测试:一个service负责插入,一个service负责查询。负责插入的service要模拟延迟。
package com.powernode.bank.service.impl; import com.powernode.bank.dao.AccountDao; import com.powernode.bank.pojo.Account; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; /** * @author 动力节点 * @version 1.0 * @className IsolationService1 * @since 1.0 **/ @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); } }
package com.powernode.bank.service.impl; import com.powernode.bank.dao.AccountDao; import com.powernode.bank.pojo.Account; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * @author 动力节点 * @version 1.0 * @className IsolationService2 * @since 1.0 **/ @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); }
通过执行结果可以清晰的看出隔离级别不同,执行效果不同。
- 读未提交:READ_UNCOMMITTED
事务超时
-
代码如下:
@Transactional(timeout = 10)
-
以上代码表示设置事务超时时间为10秒。
表示超过10秒如果该事务中所有的DML语句还没有执行完毕的话,最终会选择回滚
默认值为-1表示没有时间限制。
在当前事务中,最后一条DML语句执行之前的时间。如果最后一条DML语句后面还有很多业务逻辑,这些业务逻辑代码执行时间不被记入超时时间。
@Transactional(timeout = 10) // 设置事务超时时间为10秒。 public void save(Account act) { accountDao.insert(act); // 睡眠一会 try { //这里的代码超时时间不会被计入事务超时时间,因为以下的代码没有DML语句了。 Thread.sleep(1000 * 15); } catch (InterruptedException e) { e.printStackTrace(); } }
@Transactional(timeout = 10) // 设置事务超时时间为10秒。 public void save(Account act) { // 睡眠一会 try { //因为以下的代码中还有DML语句,所以这里的睡眠会被计入超时时间 Thread.sleep(1000 * 15); } catch (InterruptedException e) { e.printStackTrace(); } accountDao.insert(act); }
如果想让整个方法所有的代码都计入超时时间的话,可以在方法最后一行加上一句无关紧要的DML语句
只读事务
-
代码如下:
@Transactional(readOnly = true)
将当前事务设置为只读事务,在该事务执行过程中只允许select语句执行,如果在只读事务中出现delete、insert、update语句,则抛出异常。
-
将事务设置为“只读事务”的特性是:spring内部会,启动spring的优化策略。提高select语句执行效率。
-
如果事务中确实没有增删改操作,那么建议将事务设置为只读事务。
设置那些异常回滚事务
-
不设置的话,表示所有异常都回滚事务。
-
代码如下:
@Transactional(rollbackFor = RuntimeException.class)
-
表示只有发生RuntimeException异常或者该异常的子类异常才会回滚事务。
这只那些异常不回滚事务
-
代码如下:
@Transactional(noRollbackFor = NullPointerException.class)
表示只有发生NullPointerException或者该异常的子类异常不回滚,其他异常都回滚。
全注解式开发
-
编写一个类来代替配置文件:
package com.powernode.bank; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; /** * @author 动力节点 * @version 1.0 * @className Spring6Config * @since 1.0 **/ @Configuration //代替xml配置文件 @ComponentScan("com.powernode.bank") //扫描组件 @EnableTransactionManagement //开启事务注解驱动 public class Spring6Config { //spring框架看到@Bean注解之后,会调用这个被注解标注的方法,这个方法的返回值是一个Java对象,这个Java对象会自动纳入IoC容器的管理。 //返回的对象就是spring容器当中的一个Bean了。 //这个Bean的名字是dateSource @Bean(name = "dataSource") 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,所以spring会去调用这个方法,spring调用这个方法的时候发现这个方法有一个DataSource类型的参数,spring会根据类型进行自动注入。 @Bean(name = "jdbcTemplate") public JdbcTemplate getJdbcTemplate(DataSource dataSource){ JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } //如果使用@Bean注解声明Bean的时候,没有使用Bean注解的name属性指定Bean的名字的时候,默认方法名就是Bean的名字 @Bean public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){ DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource); return dataSourceTransactionManager; } }
-
测试程序如下:
@Test public void testNoXml(){ ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class); AccountService accountService = applicationContext.getBean("accountService", AccountService.class); try { accountService.transfer("act-001", "act-002", 10000); System.out.println("转账成功"); } catch (Exception e) { e.printStackTrace(); } }
-
运行结果:
声明式事务之XML实现方式
-
第一步:配置事务管理器。【DataSourceTransactionManager】
-
第二步:配置通知。
-
第三步:配置切面。
-
记得添加AspectJ依赖。
<!--aspectj依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>6.0.0-M2</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" 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>
- 将AccountServiceImpl类上的@Transactional注解删除
-
编写测试程序:
@Test public void testTransferXml(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml"); AccountService accountService = applicationContext.getBean("accountService", AccountService.class); try { accountService.transfer("act-001", "act-002", 10000); System.out.println("转账成功"); } catch (Exception e) { e.printStackTrace(); } }
Spring6整合JUnit5
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
package com.powernode.spring6.bean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author 动力节点
* @version 1.0
* @className User
* @since 1.0
**/
@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>
-
测试程序:
package com.powernode.spring6.test; import com.powernode.spring6.bean.User; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * @author 动力节点 * @version 1.0 * @className SpringJUnit4Test * @since 1.0 **/ @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。比较方便。
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>
-
单元测试:
package com.powernode.spring6.test; import com.powernode.spring6.bean.User; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @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”)
Spring6集成MyBatis3.5
实现步骤
-
第一步:准备数据表。
- 使用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接口的实现类。
- AccountSerivce
- AccountServiceImpl
-
第八步:编写jdbc.properties
- 数据库连接池相关信息。
-
第九步:编写mybatis-config.xml配置文件。
- 该文件可以没有,大部分配可以转移到spring配置文件中。
- 如果遇到mybatis相关的系统级配置,还是需要这个文件。
-
第十步:编写spring.xml配置文件
- 组件扫描。
- 引入外部属性文件。
- 数据源
- SqlSessionFactoryBean配置
- 注入mybatis核心配置文件的路径
- 指定别名包。
- 注入数据源
- Mapper扫描配置器
- 指定扫描的包
- 事务管理器DataSourceTransactionManager
- 注入数据源
- 启用事务注解驱动器
- 注入书屋管理器
-
第十一步:编写测试程序,并且添加事务进行测试。
具体实现
-
第一步:准备数据表
-
第二步: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
package com.powernode.bank.pojo; /** * @author 动力节点 * @version 1.0 * @className Account * @since 1.0 **/ 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接口
package com.powernode.bank.mapper; import com.powernode.bank.pojo.Account; import java.util.List; /** * @author 动力节点 * @version 1.0 * @className AccountMapper * @since 1.0 **/ 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接口的实现类
package com.powernode.bank.service; import com.powernode.bank.pojo.Account; import java.util.List; /** * @author 动力节点 * @version 1.0 * @className AccountService * @since 1.0 **/ 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); }
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")//纳入Spring容器的管理 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.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配置文件【注意:当你在spring.xml文件中直接写标签内容时,IDEA会自动给你添加命名空间】
<?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>
-
在以上的代码中,bean标签配置了MapperScannerConfigurer,它是MyBatis提供的用于扫描包中所有Mapper接口并创建代理类的类。当应用程序启动时,Spring IoC容器会先实例化SqlSessionFactoryBean,并将dataSource、configLocation和typeAliasesPackage等属性注入进去。然后,容器会再实例化MapperScannerConfigurer,将basePackage属性注入进去。此时,MapperScannerConfigurer就会根据basePackage指定的包路径,扫描该包及其子包下所有的Mapper接口。然后,它会为这些Mapper接口创建代理对象,并将这些代理对象交给Spring IoC容器管理。这样,在应用程序运行时,只需要使用@Autowired或@Resource等注解即可将Mapper接口的代理对象注入到需要使用它的地方。
-
<bean class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property> <property name="configLocation" value="mybatis-config.xml"></property> <property name="typeAliasesPackage" value="com.xyh.bank.pojo"></property> </bean> <!-- 这段代码是在使用 MyBatis 和 Spring 进行整合时,配置 SqlSessionFactory 的 Bean。 具体来说,<bean> 标签是 Spring 中定义 Bean 的标准元素,它的 class 属性指定了要创建的 Bean 的类。在这个例子中,org.mybatis.spring.SqlSessionFactoryBean 是 MyBatis-Spring 库中提供的一个工厂类,用于创建 MyBatis 的 SqlSessionFactory 对象。 这个 Bean 中定义了三个属性: dataSource:指定了数据源对象,也就是连接数据库所需要的信息,这个属性是必须的。 configLocation:指定了 MyBatis 的配置文件位置,这个配置文件中包含了一些全局性的配置项,比如类型别名、插件、拦截器等等。如果不指定这个属性,默认会在 classpath 下寻找名为 mybatis-config.xml 的文件。 typeAliasesPackage:指定了 POJO 类所在的包路径,用于自动扫描并注册类型别名。这个属性可以省略,但是如果不指定的话,MyBatis 就需要通过手动设置 typeAliases 属性来注册类型别名了。 以上三个属性都是用来配置 SqlSessionFactoryBean 的,通过这个 Bean 可以将其注入到其他需要使用它的地方,比如 Mapper 接口或者自定义的 DAO 实现类中,以便进行数据库操作。 -->
-
-
第十一步:编写测试程序,并添加事务,进行测试
package com.powernode.spring6.test; import com.powernode.bank.service.AccountService; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author 动力节点 * @version 1.0 * @className SMTest * @since 1.0 **/ 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("转账失败"); } } }
重要结论:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.powernode.bank.mapper"/>
</bean>
-
这个在扫描的时候他是怎样区分是不是mapper接口的?还是将这个包下所有的接口都当作是mapper接口?
- 这段代码是使用 MyBatis 和 Spring 进行整合时,配置扫描 Mapper 接口的 Bean。
- 具体来说,
<bean>
标签是 Spring 中定义 Bean 的标准元素,它的class
属性指定了要创建的 Bean 的类。在这个例子中,org.mybatis.spring.mapper.MapperScannerConfigurer
是 MyBatis-Spring 库中提供的一个类,用于自动扫描接口并将其注册为 Mapper。 - 这个 Bean 中定义了一个属性:
basePackage
:指定了要扫描的包路径,所有在该路径下的接口都会被自动扫描并注册为 Mapper。
默认情况下,MyBatis-Spring 会将所有的接口都认为是 Mapper 接口,并进行注册。但是,如果你想要区分哪些接口是 Mapper 接口,可以通过给 Mapper 接口添加特殊注解来实现。例如,可以在 Mapper 接口上添加
@Mapper
注解,告诉 MyBatis-Spring 这是一个 Mapper 接口。然后,在<bean>
标签中加入annotationClass
属性,指定要扫描的注解类型,如下所示:<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.powernode.bank.mapper"/> <property name="annotationClass" value="com.example.annotation.Mapper"/> </bean>
这样,MyBatis-Spring 就只会扫描标记了
@Mapper
注解的接口,并将其注册为 Mapper。
Spring配置文件中的import
-
spring的配置文件有多个,并且可以在spring的核心配置文件中使用import进行引入,我们可以将组件扫描单独定义到一个配置文件中,如下:
<?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>
-
然后在核心配置文件中引入:
<?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单独配置到一个文件夹中,然后再核心配置文件中引入。
Spring中的八大模式
简单工程模式
- BeanFactory的getBean()方法,通过唯一标识来获取Bean对象。是典型的简单工厂模式(静态工厂模式);
工厂模式
- FactoryBean是典型的工厂方法模式。在配置文件中通过factory-method属性来指定工厂方法,该方法是一个实例方法。
单例模式
-
Spring用的是双重判断加锁的单例模式。请看下面代码,我们之前讲解Bean的循环依赖的时候见过:
代理模式
- Spring的AOP就是使用了动态代理实现的。
装饰器模式
-
JavaSE中的IO流是非常典型的装饰器模式。
Spring 中配置 DataSource 的时候,这些dataSource可能是各种不同类型的,比如不同的数据库:Oracle、SQL Server、MySQL等,也可能是不同的数据源:比如apache 提供的org.apache.commons.dbcp.BasicDataSource、spring提供的org.springframework.jndi.JndiObjectFactoryBean等。
这时,能否在尽可能少修改原有类代码下的情况下,做到动态切换不同的数据源?此时就可以用到装饰者模式。
Spring根据每次请求的不同,将dataSource属性设置成不同的数据源,以到达切换数据源的目的。
Spring中类名中带有:Decorator和Wrapper单词的类,都是装饰器模式。
观察者模式
-
定义对象间的一对多的关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。Spring中观察者模式一般用在listener的实现。
Spring中的事件编程模型就是观察者模式的实现。在Spring中定义了一个ApplicationListener接口,用来监听Application的事件,Application其实就是ApplicationContext,ApplicationContext内置了几个事件,其中比较容易理解的是:ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent
策略模式
-
策略模式是行为性模式,调用不同的方法,适应行为的变化 ,强调父类的调用子类的特性 。
getHandler是HandlerMapping接口中的唯一方法,用于根据请求找到匹配的处理器。
比如我们自己写了AccountDao接口,然后这个接口下有不同的实现类:AccountDaoForMySQL,AccountDaoForOracle。对于service来说不需要关心底层具体的实现,只需要面向AccountDao接口调用,底层可以灵活切换实现,这就是策略模式。
模板方法模式
- Spring中的JdbcTemplate类就是一个模板类。它就是一个模板方法设计模式的体现。在模板类的模板方法execute中编写核心算法,具体的实现步骤在子类中完成。
");
} catch (Exception e) {
e.printStackTrace();
System.out.println(“转账失败”);
}
}
}
```
重要结论:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.powernode.bank.mapper"/>
</bean>
-
这个在扫描的时候他是怎样区分是不是mapper接口的?还是将这个包下所有的接口都当作是mapper接口?
- 这段代码是使用 MyBatis 和 Spring 进行整合时,配置扫描 Mapper 接口的 Bean。
- 具体来说,
<bean>
标签是 Spring 中定义 Bean 的标准元素,它的class
属性指定了要创建的 Bean 的类。在这个例子中,org.mybatis.spring.mapper.MapperScannerConfigurer
是 MyBatis-Spring 库中提供的一个类,用于自动扫描接口并将其注册为 Mapper。 - 这个 Bean 中定义了一个属性:
basePackage
:指定了要扫描的包路径,所有在该路径下的接口都会被自动扫描并注册为 Mapper。
默认情况下,MyBatis-Spring 会将所有的接口都认为是 Mapper 接口,并进行注册。但是,如果你想要区分哪些接口是 Mapper 接口,可以通过给 Mapper 接口添加特殊注解来实现。例如,可以在 Mapper 接口上添加
@Mapper
注解,告诉 MyBatis-Spring 这是一个 Mapper 接口。然后,在<bean>
标签中加入annotationClass
属性,指定要扫描的注解类型,如下所示:<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.powernode.bank.mapper"/> <property name="annotationClass" value="com.example.annotation.Mapper"/> </bean>
这样,MyBatis-Spring 就只会扫描标记了
@Mapper
注解的接口,并将其注册为 Mapper。
Spring配置文件中的import
-
spring的配置文件有多个,并且可以在spring的核心配置文件中使用import进行引入,我们可以将组件扫描单独定义到一个配置文件中,如下:
<?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>
-
然后在核心配置文件中引入:
<?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单独配置到一个文件夹中,然后再核心配置文件中引入。
Spring中的八大模式
简单工程模式
- BeanFactory的getBean()方法,通过唯一标识来获取Bean对象。是典型的简单工厂模式(静态工厂模式);
工厂模式
- FactoryBean是典型的工厂方法模式。在配置文件中通过factory-method属性来指定工厂方法,该方法是一个实例方法。
单例模式
-
Spring用的是双重判断加锁的单例模式。请看下面代码,我们之前讲解Bean的循环依赖的时候见过:
[外链图片转存中…(img-p5oRVLfc-1686488705250)]
代理模式
- Spring的AOP就是使用了动态代理实现的。
装饰器模式
-
JavaSE中的IO流是非常典型的装饰器模式。
Spring 中配置 DataSource 的时候,这些dataSource可能是各种不同类型的,比如不同的数据库:Oracle、SQL Server、MySQL等,也可能是不同的数据源:比如apache 提供的org.apache.commons.dbcp.BasicDataSource、spring提供的org.springframework.jndi.JndiObjectFactoryBean等。
这时,能否在尽可能少修改原有类代码下的情况下,做到动态切换不同的数据源?此时就可以用到装饰者模式。
Spring根据每次请求的不同,将dataSource属性设置成不同的数据源,以到达切换数据源的目的。
Spring中类名中带有:Decorator和Wrapper单词的类,都是装饰器模式。
观察者模式
-
定义对象间的一对多的关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。Spring中观察者模式一般用在listener的实现。
Spring中的事件编程模型就是观察者模式的实现。在Spring中定义了一个ApplicationListener接口,用来监听Application的事件,Application其实就是ApplicationContext,ApplicationContext内置了几个事件,其中比较容易理解的是:ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent
策略模式
-
策略模式是行为性模式,调用不同的方法,适应行为的变化 ,强调父类的调用子类的特性 。
getHandler是HandlerMapping接口中的唯一方法,用于根据请求找到匹配的处理器。
比如我们自己写了AccountDao接口,然后这个接口下有不同的实现类:AccountDaoForMySQL,AccountDaoForOracle。对于service来说不需要关心底层具体的实现,只需要面向AccountDao接口调用,底层可以灵活切换实现,这就是策略模式。
模板方法模式
- Spring中的JdbcTemplate类就是一个模板类。它就是一个模板方法设计模式的体现。在模板类的模板方法execute中编写核心算法,具体的实现步骤在子类中完成。