什么是Spring?
优点
- 轻量级的开源框架
- 轻量级:非侵入性。基于spring框架开发的类可以不依赖于spring的API
- 作用:管理java对象,控制对象的生命周期(IOC)
- MVC三层架构
- 控制层 controller
- 业务层 service
- 持久层 dao
- 控制反转IOC、面向切面AOP
- 支持事务、整合现有的框架技术
- 一句话概述:Spring是一个轻量级的控制反转(IOC)和面向切面(AOP)的容器(框架)
Spring的组成(七个模块)
-
Spring框架是一个分层架构,由七个模块组成
-
Spring模块构建在核心容器之上
-
每个模块(或组件)可以单独存在,也可以和其他模块联合使用
-
1.核心容器
- 核心容器定义了创建、配置和管理bean的方式,提供 Spring 框架的基本功能
- 核心容器的主要组件是 BeanFactory ,BeanFactory 使用控制反转(IOC)模式将应用程序的配置和依赖性规范与实际的应用程序代码分开
- 核心容器包括
- spring-core:模块提供了框架的基本组成部分,包括 IoC 和依赖注入功能
- spring-beans:提供 BeanFactory
- spring-context:Context 模块继承自 Bean 模块,并且添加了国际化(比如,使用资源束)、事件传播、资源加载和透明地创建上下文(比如,通过 Servelet 容器)等功能
- spring-context-support:提供了强大的表达式语言,用于在运行时查询和操作对象图
- spring-expression(SpEL,Spring 表达式语言,Spring Expression Language)
- 等模块组成
-
2.Spring上下文
- Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息
- 包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能
-
3.Spring AOP
- 通过配置管理特性,Spring AOP 模块直接将面向切面的编程功能 , 集成到了 Spring 框架中
- 所以,可以很容易地使 Spring 框架管理任何支持 AOP的对象
- Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务
- 通过使用 Spring AOP,不用依赖组件,就可以 将声明性事务管理集成到应用程序中
-
4.Spring DAO
- JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息
- 异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)
- Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构
-
5.Spring ORM
- Spring框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具
- 其中包括 JDO、Hibernate 和 iBatis SQL Map,所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构
-
6.Spring Web模块
- Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文
- 所以,Spring 框架支持与 Jakarta Struts 的集成
- Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作
-
7.Spring MVC 框架
- MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现
- 通过策略接口,MVC 框架变成为高度可配置的
- MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI
IOC(控制反转)
- 控制反转IoC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IoC的一种方法
- 控制反转:将开发者手中的对Java对象的控制权交给Spring框架的过程。通过配置Bean实现
- IOC容器:存放Spring创建出来对象的一个容器
- IoC是Spring框架的核心内容,可以使用XML配置,也可以使用注解,新版本的Spring也可以零配置实现IoC
- 控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式
- 在Spring中实现控制反转的是IoC容器,其实现方法是依赖注入(Dependency Injection,DI)
bean标签
属性介绍
- id
- 是 bean的唯一标识
- 整个IOC 容器 id 值不允许重复,使用名称作为key
- name
- 一个bean的名称,可以存在多个,多个之间使用逗号分隔
- 如果没有定义name属性,默认id都会当做name。
- class
- bean的具体的类型,包名和类名组成。
- scope
- bean的作用域
- prototype
- 非单例,每次获取都会创建一个新的bean对象。
- singleton
- 单例,多次获取永远同一个bean, 默认值。
- request
- 一次请求,基于web项目的bean的作用域。
- session
- 一次会话,基于web项目的bean的作用域。
- lazy-init
- 延迟初始化
- 默认加载配置文件时,bean对象就会被初始化,
- azy-init则是获取时才会初始化
- 只针对单例模式有效,非单例每次获取都会创建,没有延迟初始化的意义
- depends-on
- 初始化时依赖的对象
- 当前对象初始化前需先初始化depends-on指定的对象
- init-method
- 对象初始化后,调用的方法
- destroy-method
- 对象销毁时,调用的方法
- autowire
- 属性自动装配
- byName
- 根据属性名称装配
- byType
- 根据类型装配
- autowire-candidate
- 是否允许作为自动装配的候选项
- true 作为自动装配的候选项
- false 不作为自动装配的候选项
- primary
- 优先使用该bean
- 因为Spring需要支持使用类型查找对象,在一个大类型下,可能存在多个小类型
- 如果根据大类型装配属性时,不知道使用哪个具体的对象,则可以根据primary
使用流程
-
maven依赖配置
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.18</version> </dependency>
-
applicationContext.xml
<!--一个bean标签就是一个对象--> <bean id="hello1" class="com.dx.bean.HelloSpring"/> <!--当有多个同类型的bean时,使用类型获取时会报错,需要设置primary属性--> <bean id="hello" class="com.dx.bean.HelloSpring" primary="true"/>
-
test.java
/** * 获取对象,并调用其方法 * */ @Test public void iocTest(){ //1.初始化ioc容器 ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); //2.从容器中取出对象 //方式1:通过name获取Bean对象,如果有多个此类型的bean对象时,获取标识的主bean,如果没有标识则抛异常 HelloSpring hello = (HelloSpring) ac.getBean("hello"); //方式2:通过bean的类型获取Bean对象 HelloSpring hello1 = ac.getBean(HelloSpring.class); //方式3:通过name和类型获取Bean对象 HelloSpring hello2 = ac.getBean("hello",HelloSpring.class); hello.show(); }
bean的作用域
-
singleton: 单例,默认
- 一个bean标签在整个IOC容器中仅会创建一个对象
- 在IOC容器创建完成之后,立即创建,无论是否使用到此bean
-
prototype: 原型(多例)
- 一个bean标签在整个IOC容器中会创建多个对象
- 在IOC容器初始化时不会创建任何对象,只有在使用bean时,才会去创建对象,每次创建的都是新对象
-
用法
<bean id="book" class="com.bjpowernode.bean.Book" scope="singleton"/> <bean id="book" class="com.bjpowernode.bean.Book" scope="prototype"/>
bean的生命周期
-
通过构造方法创建Bean对象
-
为Bean对象中的成员变量赋值
-
调用Bean对象中初始化方法(init-method)
-
Bean对象的使用
-
当容器关闭之前,调用Bean对象的销毁方法(destroy-method)
-
applicationContext.xml
<!--需要设定初始化方法和销毁方法,否则不执行--> <bean id="dog" class="com.bjpowernode.bean.Dog" init-method="init" destroy-method="destroy"> <property name="name" value="哈士奇"/> </bean>
-
Dog.java
public class Dog { private String name; public Dog(){ System.out.println("Dog对象被创建..."); } public void init(){ System.out.println("Dog对象的初始化方法..."); } public String getName() { return name; } public void setName(String name) { System.out.println("name属性被赋值..."); this.name = name; } public void destroy(){ System.out.println("Dog对象的销毁方法..."); } }
-
test.java
@Test public void lifeCycle() { System.out.println("=============IOC容器初始化阶段==============="); ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); System.out.println("=============对象使用阶段==============="); Dog dog = ac.getBean("dog", Dog.class); System.out.println(dog); System.out.println(dog.getName()); System.out.println("=============IOC容器关闭阶段==============="); ac.close(); }
depends-on属性
-
先创建指定的bean,再创建自己
-
<!--depends-on 会创建好指定的bean后再创建该bean--> <bean id="emp" class="com.dx.bean.Emp" depends-on="dept"/> <bean id="dept" class="com.dx.bean.Dept"/>
创建bean对象的四种方式
-
构造方法
-
通过反射机制调用类中无参或有参构造方法来是实现Bean的创建
-
<!--使用构造方法创建 (Spring默认的创建方式)--> <bean id="cat" class="com.dx.bean2.Cat"/>
-
public class Cat { public String name; @Override public String toString() { return name + "猫咪对象"; } }
-
测试类(下同)
@Test public void factory() { ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); Cat cat = ac.getBean("cat", Cat.class); System.out.println(cat); }
-
-
静态工厂,实例工厂
-
通过一个工厂类来间接实现对象创建,将Bean对象放置在IOC容器
-
<!--静态工厂 factory-method指定获取对象的方法名--> <bean id="cat1" class="com.dx.factory.CatStaticFactory" factory-method="getInstance"/>
/* Cat对象的静态工厂 */ public class CatStaticFactory { public static Cat getInstance(){ Cat cat = new Cat(); cat.name = "加菲猫"; return cat; } }
-
<!--实例工厂--> <bean id="factory" class="com.dx.factory.CatInstanceFactory"/> <bean id="cat2" factory-method="getInstance" factory-bean="factory"/>
/* Cat对象的实例工厂 */ public class CatInstanceFactory { public Cat getInstance(){ Cat cat = new Cat(); cat.name = "黑猫警长"; return cat; } }
-
-
FactoryBean接口
-
<!--FactoryBean--> <bean id="cat3" class="com.dx.factory.CatFactoryBean"/>
/* 实现FactoryBean接口 */ public class CatFactoryBean implements FactoryBean<Cat> { /*获取对象*/ @Override public Cat getObject() throws Exception { Cat cat = new Cat(); cat.name = "小花猫"; return cat; } /*获取对象的Class类型*/ @Override public Class<?> getObjectType() { return Cat.class; } /*配置当前对象是否为单例模式*/ @Override public boolean isSingleton() { return true; } }
-
-
注解
略,在后面
依赖注入(DI)
-
Dependency Injection
-
依赖:在A类中使用B类:A类对B类产生依赖关系。
-
public class Classes{} public class Student{ String sname; Integer age; Double score; Classes classes; public static void main(String[] args){ Student s1 = new Student(); } }
-
可以说Student类对String, Integer, Double, Classes类产生了依赖关系
-
-
注入:为依赖类型的变量赋值
-
所谓控制反转就是:获得依赖对象的方式反转了
注入属性的类型
- 简单类型数据: 使用字符串可以表示的数据 int, double, boolean, Date
- 自定义对象(POJO)
- 集合,数组
属性注入的三种方式
-
构造方法
public class Car { private Integer id; private String name; private String type; private Double price; public Car(Integer id, String name, String type, Double price) { this.id = id; this.name = name; this.type = type; this.price = price; } }
<!--构造方法注入--> <bean id="car" class="com.dx.bean2.Car"> <!--默认按照参数顺序注入,乱序会报错--> <constructor-arg value="001"/> <constructor-arg value="奥迪"/> <constructor-arg value="轿车"/> <constructor-arg value="49.0"/> <!-- 可以使用属性来指定构造方法的参数 value 构造方法参数值 index 构造方法参数索引 name 构造方法参数名称(推荐) type 构造方法参数类型 --> <!--使用index--> <constructor-arg value="奥迪" index="1"/> <constructor-arg value="001" index="0"/> <constructor-arg value="49.0" index="3"/> <constructor-arg value="轿车" index="2"/> <!--使用name--> <constructor-arg value="奥迪" name="name"/> <constructor-arg value="001" name="id"/> <constructor-arg value="49.0" name="price"/> <constructor-arg value="轿车" name="type"/> </bean>
-
set方法
-
public class Student { private String name; private Integer age; private Double score; private Boolean gender; private Date birthday; private Address address; private String[] courseNames; private List<String> list; private Set<Integer> set; private Map<String, Double> map; private Properties properties; //set和get方法这里省略了 //...... } public class Address { private String city; private String area; private String street; //set...... get...... }
<!--set方法注入--> <bean id="student" class="com.dx.bean2.Student"> <!--name中配置的不是成员变量名称,而是set方法的名称(去除set之后首字母小写后的名称)--> <property name="name" value="小明"/> <property name="age" value="22"/> <property name="score" value="86.5"/> <property name="gender" value="true"/> <!--spring中默认支持的日期格式为yyyy/MM/dd--> <property name="birthday" value="1998/05/19"/> <!--自定义类型的数据依赖注入: ref属性配置IOC容器中的beanName--> <!--方式1 --> <!-- <property name="address" ref="address"/>--> <!--方式2 --> <!-- <property name="address">--> <!-- <ref bean="address"/>--> <!-- </property>--> <!--方式3:内部bean--> <property name="address"> <bean class="com.dx.bean2.Address"> <property name="city" value="北京市"/> <property name="area" value="海淀区"/> <property name="street" value="人民路"/> </bean> </property> <!--数组--> <property name="courseNames"> <array> <value>语文</value> <value>数学</value> <value>外语</value> </array> </property> <!--list集合--> <property name="list"> <list> <value>aaa</value> <value>bbb</value> <value>ccc</value> <value>ddd</value> </list> </property> <!--set集合--> <property name="set"> <set> <value>10</value> <value>20</value> <value>30</value> <value>40</value> </set> </property> <!--map集合--> <property name="map"> <map> <entry key="one" value="1.2"/> <entry key="two" value="2.6"/> <entry key="three" value="3.14"/> </map> </property> <!--properties--> <property name="properties"> <props> <prop key="jdbc.driver">com.mysql.jdbc.Driver</prop> <prop key="jdbc.url">jdbc:mysql://localhost:3306/test</prop> <prop key="jdbc.username">root</prop> <prop key="jdbc.password">123</prop> </props> </property> </bean> <bean id="address" class="com.dx.bean2.Address"> <property name="city" value="郑州市"/> <property name="area" value="金水区"/> <property name="street" value="民航路"/> </bean>
-
-
注解
略,在后面
p/c命名空间注入(扩展)
-
P命名空间注入(注入属性: properties)
导入约束 : xmlns:p="http://www.springframework.org/schema/p" <!--P命名空间 , 属性依然要设置set方法--> <bean id="user" class="com.dx.pojo.User" p:name="dx" p:age="18"/>
-
C命名空间注入(注入构造方法的参数: Constructor)
导入约束 : xmlns:c="http://www.springframework.org/schema/c" <!--C命名空间 , 属性依然要设置set方法--> <bean id="user" class="com.dx.pojo.User" c:name="dx" c:age="18"/>
自动装配
-
在进行自定类型的数据依赖注入时,IOC容器会自动查找匹配对象,实施依赖注入
-
条件:仅限于自定义类型的数据(POJO)
-
实现:autowire属性
-
类型:
-
no: 不自动装配
-
byName: 根据属性名称匹配,如果没有找到Bean,不进行自动装配,不会报错。
-
byType: 根据属性类型匹配,如果没有找到此类型Bean,不进行自动装配,不会报错。
但是如果找到多个没有标识Primary的Bean,就是报错。
-
default: 默认值,会根据父标签beans标签中default-autowire属性来取值。
-
-
缺点: 不灵活,不能实现即能根据名称匹配又能根据类型匹配
-
实现
public class Order { private String orderNum; private Double price; private Customer customer; //get set.... } public class Customer { private String username; private String password; //get set.... }
<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" default-autowire="byName"> <!--autowire自动装配--> <bean id="order" class="com.dx.bean2.Order" autowire="byType"> <property name="orderNum" value="XA20220712001"/> <property name="price" value="3998"/> </bean> <bean id="cust" class="com.dx.bean2.Customer" primary="true"> <property name="username" value="tom"/> <property name="password" value="123"/> </bean> <bean id="customer" class="com.dx.bean2.Customer"> <property name="username" value="jerry"/> <property name="password" value="321"/> </bean> </beans>
@Test public void autowire() { ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); Order order = ac.getBean("order", Order.class); System.out.println(order); }
使用注解
- 组件扫描
-
先配置命名空间和标签规范。配置在当前项目中哪些包中注解是生效的。
-
xmlns:context="http://www.springframework.org/schema/context" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
-
配置Bean
- @Component
- @Controller
- @Service
- @Repository
- 此以上四种注解在IOC模块中功能上没有区别,都是配置Bean。
- 不建议大家混用。在Spring项目中,控制层使用@Controller,业务层@Service,持久层@Repository,@Component配置除三层架构之外的类。
- @Scope:配置作用域,例如:@Scope(“prototype”)配置为多例模式
-
依赖注入
- @Value 简单类型
- @Autowired 自定义类型
- Spring中的注解。先根据类型匹配,再根据名称匹配
- @Autowired(required=false):默认为true,如果没有找到bean会报错,设为false后,不会报错
- @Qualifier(“beanName”):当有多个bean时,根据名字指定bean
- @Primary:当有多个bean切无法自动匹配时,可使用这个注解设置主bean
- @Resource 自定义类型
- jdk中的注解,spring作了扩展支持。先根据名称匹配,再根据类型匹配
-
使用方法
-
// @Controller // @Service // @Repository @Component("dd") // @Primary public class Department { @Value("10") private Integer id; @Value("研发部") private String dname; //get... set... } @Data @NoArgsConstructor @AllArgsConstructor @Component("abc") public class Employee { @Value("123") private Integer id; @Value("张三") private String ename; @Autowired @Qualifier("dd") //通过注解指定bean // @Resource private Department department; }
-
<!--使用注解--> <!--0.在beans标签中加上命名空间和标签规范 xmlns:context="http://www.springframework.org/schema/context" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd --> <!--1.扫描基础包,此包下所有类中的注解都会生效,以及此包所有子包类的注解会生效--> <context:component-scan base-package="com.dx"/> <!--配置除注解外的其他bean--> <bean id="department" class="com.dx.bean2.Department"> <property name="id" value="666"/> <property name="dname" value="李四"/> </bean>
-
@Test public void annotation() { ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); //依赖注入简单类型 Department department = ac.getBean("dd", Department.class); System.out.println(department); //依赖注入自定义类型 Employee employee = ac.getBean("abc", Employee.class); System.out.println(employee); }
-
代理模式(了解)
静态代理
-
实现方法
public interface UserDao { void insert(); void update(); void delete(int id); List<String> select(); } public class UserDaoImpl implements UserDao { @Override public void insert() { System.out.println("UserDaoImpl---->insert..........."); } @Override public void update() { System.out.println("UserDaoImpl---->update..........."); } @Override public void delete(int id) { System.out.println("UserDaoImpl---->delete,id="+id); } @Override public List<String> select() { System.out.println("UserDaoImpl---->select..........."); return Arrays.asList("111","222","333"); } }
/** * 静态代理 * */ public class UserStaticProxy implements UserDao { private UserDao userDao = null; public UserStaticProxy(UserDao userDao) { this.userDao = userDao; } @Override public void insert() { System.out.println("代理 增加的功能--->insert"); userDao.insert(); } @Override public void update() { System.out.println("代理 增加的功能--->update"); userDao.update(); } @Override public void delete(int id) { System.out.println("代理 增加的功能--->delete id="+id); userDao.delete(id); } @Override public List<String> select() { System.out.println("代理 增加的功能--->select"); return userDao.select(); } }
/** * 静态代理 * */ @Test public void StaticProxy(){ UserStaticProxy userDaoProxy = new UserStaticProxy(new UserDaoImpl()); userDaoProxy.insert(); userDaoProxy.update(); userDaoProxy.delete(10); List<String> list = userDaoProxy.select(); System.out.println(list); }
动态代理
-
动态代理的代理类是动态生成的,静态代理的代理类是我们提前写好的
-
动态代理分为两类
- 一类是基于接口动态代理----JDK动态代理
- 一类是基于类的动态代理-----cglib
-
一个动态代理 , 一般代理某一类业务,可以代理多个类,代理的是接口!
-
JdkProxy 实现方式
public class JdkProxy implements InvocationHandler { /** * 执行被代理对象中的方法 * @param proxy 被代理对象 * @param method 目标方法 * @param args 目标方法的参数值 * @return 目标方法的返回值 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我是代理类添加的辅助业务。"); //执行目标业务方法,利用反射 Object result = method.invoke(targetClass.newInstance(), args); return result; } private Class targetClass; /** * 获取代理对象方法 * 代理类对象:new JdbProxy(), 作用:生产代理对象 * 代理对象:创建代理对象Proxy.newProxyInstance(),JDK代理对象实现了目标类接口的一个种对象 */ public Object getProxy(Class targetClass) { this.targetClass =targetClass; /* * newProxyInstance(loader,interfaces,this);生成代理对象 * 三个参数分别如下 * ClassLoader loader :业务类的类加载器 * Class<?>[] interfaces:业务类实现的所有接口 * InvocationHandler h :代理类对象(此类) */ //业务类的类加载器 ClassLoader classLoader = targetClass.getClassLoader(); //业务类实现的所有接口 Class[] interfaces = this.targetClass.getInterfaces(); //生成代理对象 Object proxyObject = Proxy.newProxyInstance(classLoader, interfaces, this); return proxyObject; } }
-
CglibProxy 实现方式
/** * cglib动态代理 * 对目标没有要求,普通类就可以,不用实现接口 * 代理类是目标类的子类(父子继承) * */ public class CglibProxy implements MethodInterceptor { /** * 执行目标方法 * @param o 代理对象 * @param method 目标方法对象 * @param objects 目标方法参数值 * @param methodProxy 代理方法的代理对象 * @return 目标方法的返回值 */ @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("我是代理类添加的辅助业务。"); //执行目标业务方法 Object result = method.invoke(targetClass.newInstance(), objects); return result; } private Class targetClass; /** * 获取代理对象方法 */ public Object getProxy(Class targetClass) { this.targetClass =targetClass; Enhancer enhancer = new Enhancer(); //目标对象设置为代理对象的父类 enhancer.setSuperclass(targetClass); //设置回调,调用intercept方法 enhancer.setCallback(this); return enhancer.create(); } }
-
测试类
/** * 动态代理 * 两种方式,效果一样 * */ @Test public void JdkProxy(){ //使用JdkProxy // UserDao userDaoProxy = (UserDao) new JdkProxy().getProxy(UserDaoImpl.class); //使用CglibProxy UserDao userDaoProxy = (UserDao) new CglibProxy().getProxy(UserDaoImpl.class); userDaoProxy.insert(); userDaoProxy.update(); userDaoProxy.delete(100); System.out.println(userDaoProxy.select()); }
AOP(面向切面)
概念
- 面向切面编程,是一种新的方法论,是对OOP面向对象编程的补充
- 横切关注点:跨越应用程序多个模块的方法或功能。与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 …
- 切面(Aspect):横切关注点,跨越多个模块的零散功能,组成的独立的、应用于其他模块之中的模块,是一个类
- 通知(Advice):切面需要完成的工作,是类中的一个方法
- 连接点(JoinPoint):某个程序的某个执行位置。例如:save()方法执行之前, update()方法执行之后
- 切点(PointCut):多个模块的连接点组成为切点。例如:所有service类中的所有方法执行之前
- 目标(Target):被通知对象
- 代理(Proxy):向目标对象应用通知之后创建的对象
- AOP的核心思想:在不改变原来的代码的情况下,实现了对原有功能的增强
通知类型
- 通知会根据连接点不同,分成以下四种通知,第五种是前四种的合成:
- 前置通知:方法执行之前
- 后置通知:方法执行之后。无法获取返回的数据
- 返回通知:方法返回数据之后,表示方法正常结束。可以获取返回的数据
- 异常通知:方法抛出异常之后,表示方法非正常结束。可以获取抛出的异常
- 环绕通知:以上四种通知的综合性通知
简单实现AOP
-
引入依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.21</version> </dependency>
-
切面类
/** * 切面类,用来记录日志 * 1.无需实现任何接口 * 2.无需使用反射机制调用目标方法 * 3.无侵入 * */ public class LogAspect { //JoinPoint对象封装了Aop中切面方法的信息 public void beforeMethod(JoinPoint joinPoint){ System.out.println("我是前置通知日志..."); } public void afterMethod(JoinPoint joinPoint){ System.out.println("我是后置通知日志..."); } public void returnMethod(JoinPoint joinPoint,Object result){ System.out.println("我是返回通知日志...返回值:"+result); } public void throwMethod(JoinPoint joinPoint,Exception e){ System.out.println("我是异常通知日志...异常为:"+e.getMessage()); } //环绕通知 public void aroundMethod(ProceedingJoinPoint proceedingJoinPoint) { System.out.println("环绕通知的前置配置"); try { Object proceed = proceedingJoinPoint.proceed(); System.out.println("环绕通知的返回结果="+proceed); } catch (Throwable e) { System.out.println("环绕通知的异常=" + e.getMessage()); } System.out.println("环绕通知的后置配置"); } }
-
写applicationContext.xml
-
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" 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 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 0.添加新的命名空间和标签规范 xmlns:aop="http://www.springframework.org/schema/aop" http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd --> <!--1.配置bean--> <bean id="user" class="com.dx.dao.impl.UserDaoImpl"/> <bean id="role" class="com.dx.dao.impl.RoleDaoImpl"/> <bean id="log" class="com.dx.aspect.LogAspect"/> <!--2.配置切面,需要添加新的命名空间和标签规范--> <aop:config> <!--写一个公共的切面表达式,可以直接调用--> <aop:pointcut id="exp" expression="execution(* com.dx.dao.impl.*.*(..))"/> <!--指定bean对象为切面对象--> <aop:aspect ref="log"> <!--配置通知--> <aop:before method="beforeMethod" pointcut="execution(* com.dx.dao.impl.*Impl.*(..))"/> <aop:after method="afterMethod" pointcut-ref="exp"/> <aop:after-returning method="returnMethod" returning="result" pointcut="execution(* com.dx.dao.impl.UserDaoImpl.select(..))"/> <aop:after-throwing method="throwMethod" throwing="e" pointcut="execution(* com.dx.dao.impl.UserDaoImpl.delete(..))"/> <aop:around method="aroundMethod" pointcut-ref="exp"/> </aop:aspect> </aop:config> </beans>
-
- 说明
-
切点表达式
编写规则:以execution()包裹 1.单个方法 语法:execution(方法访问修饰符 方法返回值类型 方法所在包名.方法所在类名.方法名(参数类型)) 例如:execution(public void com.bjpowernode.dao.impl.UserDaoImpl.insert()) 2.多个方法,通配符* 语法:execution(* *.*.*(..)) 第一星不限制方法访问修饰符和返回值类型 第二星不限制方法所在的包 第三星不限制方法所在的类 第四星不限制方法名称 两个点不限制方法的参数 例如:execution(* com.bjpowernode.dao.impl.UserDaoImpl.*(..)) 例如:execution(* com.bjpowernode.dao.impl.*Impl.*(..)) 例如:execution(* com.bjpowernode.*.service.impl.*Impl.*(..))
-
配置通知
aop:before前置通知 aop:after后置通知 aop:after-returning返回通知 aop:after-throwing异常通知 属性: method: 切面对象的方法名称 pointcut: 切点表达式, 将通知应用在那些方法上的一种表达式 pointcut-ref: 切面表达式的引用 returning: 是返回通知中的独有属性,配置接收方法的返回值参数 throwing: 是异常通知中的独有属性,配置接收方法抛出异常对象
-
使用注解实现
-
切面类
/** * 切面类 * 使用注解配置 * */ @Aspect @Order(3) //设置切面执行顺序 public class OtherAspect { //配置一个公共的切面表达式,供其他注解调用 @Pointcut("execution(* com.dx.dao.impl.*.*(..))") public void exp(){} @Before("execution(* com.dx.dao.impl.*.*(..))") public void before(){ System.out.println("Other的------前置通知..."); } @After("exp()") public void after(){ System.out.println("Other的------后置通知日志..."); } // @AfterReturning // @AfterThrowing }
-
applicationContext.xml
<!--配置bean--> <bean id="other" class="com.dx.aspect.OtherAspect"/> <!--开启AOP注解--> <aop:aspectj-autoproxy/>
多切面
- 通知采用先进后出的原则
- 前置通知先执行的切面,后置通知后执行
- 可通过order属性配置切面的执行顺序,也可以使用
@Order(1)
注解 - 属性值为整数类型,数字越小越优先
-
<aop:aspect ref="other" order="2"> <aop:before method="before" pointcut-ref="exp"/> <aop:after method="after" pointcut-ref="exp"/> </aop:aspect>
Spring与Mybatis整合
-
导入依赖
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.18</version> </dependency> <!--aop切面--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.21</version> </dependency> <!--mybaits提供的与spring整合的依赖包--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.19</version> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.4</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.1</version> </dependency> <!--分页插件--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13</version> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> </dependencies>
-
配置文件
- db.properties
#mysql jdbc.mysql.driverClassName=com.mysql.cj.jdbc.Driver jdbc.mysql.url=jdbc:mysql://localhost:3306/student?serverTimezone=UTC jdbc.mysql.username=root jdbc.mysql.password=111111
- log4j.properties
# 全局日志配置 log4j.rootLogger=DEBUG, stdout # MyBatis 日志配置 #局部日志记录配置 log4j.logger.org.mybatis.example.BlogMapper=TRACE # 控制台输出 log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p %c.%M() --%m%n ### 输出DEBUG 级别以上的日志到=E://logs/error.log ### log4j.appender.file = org.apache.log4j.DailyRollingFileAppender log4j.appender.file.File = D:/logs/test.log #log4j.appender.file.Append = true #log4j.appender.file.Threshold = DEBUG log4j.appender.file.layout = org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n ### 输出DEBUG 级别以上的日志到=E://logs/error.log ### log4j.appender.E = org.apache.log4j.DailyRollingFileAppender log4j.appender.E.File =E://logs/error.log log4j.appender.E.Append = true log4j.appender.E.Threshold = ERROR log4j.appender.E.layout = org.apache.log4j.PatternLayout log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
- db.properties
-
entity
@Data @NoArgsConstructor @AllArgsConstructor public class Emp { private Integer empno; private String ename; private String job; private Integer mgr; private Date hiredate; private Double sal; private Double comm; private Integer deptno; }
-
dao
public interface EmpDao { List<Emp> select(); Emp selectById(Integer empno); int insert(Emp entity); int update(Emp entity); int deleteById(Integer empno); }
-
EmpMapper.xml
<mapper namespace="com.dx.dao.EmpDao"> <select id="select" resultType="emp"> select empno,ename,job,mgr,hiredate,sal,comm,deptno from emp order by empno asc </select> <select id="selectById" parameterType="int" resultType="Emp"> select empno,ename,job,mgr,hiredate,sal,comm,deptno from emp where empno=#{empno} </select> <insert id="insert" parameterType="com.dx.entity.Emp"> insert into emp(ename,job,mgr,hiredate,sal,comm,deptno) values(#{ename},#{job},#{mgr},#{hiredate},#{sal},#{comm},#{deptno}) </insert> <update id="update" parameterType="com.dx.entity.Emp"> update emp set ename=#{ename},job=#{job},mgr=#{mgr},hiredate=#{hiredate},sal=#{sal},comm=#{comm},deptno=#{deptno} where empno=#{empno} </update> <delete id="deleteById" parameterType="int"> delete from emp where empno=#{empno} </delete> </mapper>
-
mybatis-config.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="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"/> </settings> <typeAliases> <package name="com.dx.entity"/> </typeAliases> <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin> </plugins> </configuration>
-
applicationContext.xml⭐
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" 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 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注解生效--> <context:component-scan base-package="com.dx"/> <!--加载第三方properties文件--> <context:property-placeholder location="classpath:db.properties"/> <!--数据源 spring-jdbc模块中提供一个内置数据源,用于测试 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.mysql.driverClassName}"/> <property name="url" value="${jdbc.mysql.url}"/> <property name="username" value="${jdbc.mysql.username}"/> <property name="password" value="${jdbc.mysql.password}"/> </bean> <!--1.将SqlSessionFactory对象交给springIOC管理--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--注入数据源--> <property name="dataSource" ref="dataSource"/> <!--加载mybatis核心配置文件--> <property name="configLocation" value="classpath:mybatis-config.xml"/> <!--加载mapper文件: 通过通配符*加载多个mapper文件--> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <!--2.mapper代理对象交给springIOC管理--> <!--a.单独配置mapper代理对象--> <bean id="empDao" class="org.mybatis.spring.mapper.MapperFactoryBean"> <!--注入SqlSessionFactory--> <property name="sqlSessionFactory" ref="sqlSessionFactory"/> <!--指定mapper接口--> <property name="mapperInterface" value="com.dx.dao.EmpDao"/> </bean> <!--b.批量配置mapper代理对象--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--注入SqlSessionFactory--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!--扫描基础包: 如果有多个持久层包,以逗号分隔即可--> <property name="basePackage" value="com.dx.dao"/> </bean> </beans>
-
EmpController.java
/** * 替代Serlvet * Controller web层:接收请求,处理请求,给予响应 */ @Controller public class EmpController { @Autowired private EmpService empService; public PageInfo<Emp> page(Integer pageNumber, Integer pageSize){ return empService.page(pageNumber, pageSize); } public List<Emp> list(){ return empService.list(); } public Emp get(Integer empno){ return empService.getById(empno); } public boolean save(Emp entity){ return empService.save(entity); } public boolean edit(Emp entity){ return empService.edit(entity); } public boolean remove(Integer empno){ return empService.remove(empno); } }
-
测试类
@Test public void pageTest(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); EmpController empController = ac.getBean(EmpController.class); PageInfo<Emp> pageInfo = empController.page(2, 5); List<Emp> list = pageInfo.getList(); for (Emp emp : list) { System.out.println(emp); } System.out.println("总页数:" + pageInfo.getPages()); System.out.println("总记录数:" + pageInfo.getTotal()); } @Test public void listTest(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); EmpController empController = ac.getBean(EmpController.class); List<Emp> list = empController.list(); for (Emp emp : list) { System.out.println(emp); } } @Test public void insertTest(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); EmpController empController = ac.getBean(EmpController.class); Emp emp = new Emp(null,"小杨","经理",100,new Date(),6500.0,1000.0,10); boolean result = empController.save(emp); System.out.println(result); } @Test public void updateTest(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); EmpController empController = ac.getBean(EmpController.class); Emp emp = new Emp(7935,"小陈","经理",100,new Date(),6500.0,1000.0,10); boolean result = empController.edit(emp); System.out.println(result); } @Test public void deleteTest(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); EmpController empController = ac.getBean(EmpController.class); boolean result = empController.remove(7937); System.out.println(result); }
事务
- 事务是一系列动作(执行SQL),一个事务是一个独立的不可拆分工作单元,事务中执行的所有SQL同时成功或同时失败
- 事务的四个特性
- 原子性 A
- 一致性 C
- 隔离性 I
- 持久性 D
编程式事务
//设置关闭自动提交
connection.setAutoCommit(false);
try{
//执行多个SQL语句
...
connection.commit();
}catch(Exception e){
connection.rollback();
}
声明式事务
-
利用AOP来管理事务,由spring来实现事务的提交或回滚。
-
声明式事务:1.配置事务管理器(切面)2.配置事务切入点
-
JDBC的事务管理器:DataSourceTransactionManager,它类似于一个切面
-
Hibernate的事务管理类器:HibernateTransactionManager
事务属性
- 事务的传播性(propagation)
- 当多个事务“相遇”(事务方法调用事务方法)时,spring该如何去处理。
- REQUIRED: 如果一个事务方法调用另一个事务方法,那么两个方法中的事务会合二为一
- REQUIRES_NEW:如果一个事务方法调用另一个事务方法,被调用事务方法会在自己独立的事务中运行
- 事务隔离级别(isolation)
-
数据库中自带的特性,在SQL标准中有四种:读未提交、读已提交、可重复读、可序列化。
-
读未提交:读取其他事务没有提交的数据,读取其他事务的内存。会引起脏读、不可重复读、幻读。
-
读已提交:仅能读取到其他事务提交的数据。可以避免脏读,但是会发生不可重复读、幻读。
-
可重复读:在一个事务操作表时,将它操作的列锁定,使其他事务无法进行列的修改。可以避免脏读、不可重复读,但是会发生幻读。
-
可序列化:在一个事务操作表时,将它操作的整个表锁定,使其他事务无法进行表的修改。可以避免所有的并发问题,但是效率低。
-
事务并发出现的三种问题:
- 脏读:TX1读取了TX2的未提交的数据,此时TX2回滚了,那么TX1读取到的数据就是无效数据。
- 不可重复读:TX读取了一个表中的字段值,再次读取时发现数据变化了。
- 幻读:TX读取了表中记录数,再次读取时发现数据量变化了。
- mysql数据库拥有以上四种事务隔离级别,默认的隔离级别是可重复读。
- oracle数据库拥有读已提交和可序列化两种,默认的事务隔离级别为读已提交。
- 注意:DEFAULT值表示spring的事务隔离级别配置,按照数据库默认隔离级别。
- 回滚列表,不回滚列表
- 只读事务
- 超时控制
- 默认执行时间没有限制
- 单位为秒
- 如果在指定时间内事务没有执行完成,就是抛出异常,事务回滚
事务的配置
声明式事务
-
@Data @NoArgsConstructor @AllArgsConstructor public class Account { private Integer id; private String name; private Double balance; }
-
public interface AccountDao { void updateBalance(Account account); Account selectById(Integer id); }
<?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.dx.dao.AccountDao"> <select id="selectById" parameterType="int" resultType="Account"> select id, name, balance from account where id=#{id} </select> <update id="updateBalance" parameterType="com.dx.entity.Account"> update account set balance=#{balance} where id=#{id} </update> </mapper>
-
public interface AccountService { /*转账*/ void transferMoney(Integer fromId, Integer toId, Double money) throws Exception; }
@Service // @Transactional(rollbackFor = Exception.class) public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; @Override public void transferMoney(Integer fromId, Integer toId, Double money) throws Exception { Account fromAccount = accountDao.selectById(fromId); Account toAccount = accountDao.selectById(toId); fromAccount.setBalance(fromAccount.getBalance()-money); accountDao.updateBalance(fromAccount); System.out.println("扣款成功"); //模拟异常:Spring默认回滚的是运行时异常,可以在配置文件或注解中配置 // System.out.println(10/0); Class.forName("aaa"); //事务超时,自动回滚 // Thread.sleep(5000); toAccount.setBalance(toAccount.getBalance()+money); accountDao.updateBalance(toAccount); System.out.println("转账完成"); } }
-
<!--******************* Spring事务 *******************--> <!--注解式事务:开启事务--> <tx:annotation-driven/> <!--声明式事务--> <!--1.事务管理器,spring提供--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!--2.事务拦截器和其属性--> <tx:advice id="transactionInterceptor" transaction-manager="transactionManager"> <tx:attributes> <!--方法名、传播行为、隔离级别、回滚异常范围、超时时间(秒)--> <tx:method name="transferMoney" propagation="REQUIRES_NEW" isolation="DEFAULT" rollback-for="java.lang.Exception" timeout="4"/> <!--方法名可以使用通配符,查询语句可以设为只读,不参与事务--> <tx:method name="query*" rollback-for="java.lang.Exception" read-only="true"/> <tx:method name="query*" rollback-for="java.lang.Exception" read-only="true"/> <tx:method name="find*" rollback-for="java.lang.Exception" read-only="true"/> <tx:method name="get*" rollback-for="java.lang.Exception" read-only="true"/> <tx:method name="list*" rollback-for="java.lang.Exception" read-only="true"/> <tx:method name="page*" rollback-for="java.lang.Exception" read-only="true"/> <tx:method name="add*" rollback-for="java.lang.Exception"/> <tx:method name="save*" rollback-for="java.lang.Exception"/> <tx:method name="insert*" rollback-for="java.lang.Exception"/> <tx:method name="edit*" rollback-for="java.lang.Exception"/> <tx:method name="update*" rollback-for="java.lang.Exception"/> <tx:method name="delete*" rollback-for="java.lang.Exception"/> <tx:method name="del*" rollback-for="java.lang.Exception"/> <tx:method name="remove*" rollback-for="java.lang.Exception"/> <tx:method name="*"/> </tx:attributes> </tx:advice> <!--3.事务切入点--> <aop:config> <aop:pointcut id="exp" expression="execution(* com.dx.service.impl.*Impl.*(..))"/> <aop:advisor advice-ref="transactionInterceptor" pointcut-ref="exp"/> </aop:config>
注解式事务
-
public interface AccountService { /*多商品一起付款*/ void payOrder(Integer fromId, Map<Integer,Double> map) throws Exception; }
@Service @Transactional(rollbackFor = Exception.class) public class AccountServiceImpl implements AccountService { //注入自己 @Autowired private AccountService accountService; @Override public void transferMoney(Integer fromId, Integer toId, Double money) throws Exception { Account fromAccount = accountDao.selectById(fromId); Account toAccount = accountDao.selectById(toId); fromAccount.setBalance(fromAccount.getBalance()-money); accountDao.updateBalance(fromAccount); System.out.println("扣款成功"); toAccount.setBalance(toAccount.getBalance()+money); accountDao.updateBalance(toAccount); System.out.println("转账完成"); } @Override //注解式事务 @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, rollbackForClassName = "java.lang.Exception", readOnly = true, timeout = 5) public void payOrder(Integer fromId, Map<Integer, Double> map) throws Exception { Set<Integer> toIdSet = map.keySet(); System.out.println(toIdSet.size()); for (Integer toId : toIdSet) { System.out.println("toId="+toId); double money = map.get(toId); accountService.transferMoney(fromId,toId,money); //模拟异常 String s=null; s.trim(); } System.out.println("订单支付成功"); } }
-
<!--注解式事务:开启事务--> <tx:annotation-driven/>
mId, Map<Integer,Double> map) throws Exception;
}
```java
@Service
@Transactional(rollbackFor = Exception.class)
public class AccountServiceImpl implements AccountService {
//注入自己
@Autowired
private AccountService accountService;
@Override
public void transferMoney(Integer fromId, Integer toId, Double money) throws Exception {
Account fromAccount = accountDao.selectById(fromId);
Account toAccount = accountDao.selectById(toId);
fromAccount.setBalance(fromAccount.getBalance()-money);
accountDao.updateBalance(fromAccount);
System.out.println("扣款成功");
toAccount.setBalance(toAccount.getBalance()+money);
accountDao.updateBalance(toAccount);
System.out.println("转账完成");
}
@Override
//注解式事务
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED,
rollbackFor = Exception.class, rollbackForClassName = "java.lang.Exception",
readOnly = true, timeout = 5)
public void payOrder(Integer fromId, Map<Integer, Double> map) throws Exception {
Set<Integer> toIdSet = map.keySet();
System.out.println(toIdSet.size());
for (Integer toId : toIdSet) {
System.out.println("toId="+toId);
double money = map.get(toId);
accountService.transferMoney(fromId,toId,money);
//模拟异常
String s=null;
s.trim();
}
System.out.println("订单支付成功");
}
}
-
<!--注解式事务:开启事务--> <tx:annotation-driven/>