八、AOP编程
8.1、静态代理
8.1.1、问题引入
为什么需要代理设计模式
在JavaEE分层开发中,Service层(业务层)对我们来说是最重要的。
Service层中包含哪些代码
在Service中会出现两种类型的代码:
- 核心功能:业务运算、Dao操作。
- 附加功能(代码量少且不属于核心功能,可有可无):事务、日志、性能监控。
8.1.2、代理设计模式概述
目标类(原始类):类似于现实生活中的房东,指的是包含核心功能的业务类。
目标方法(原始方法):目标类(原始类)中的方法。
通过代理类,为原始类(目标类)增加额外的功能,好处是有利于原始类(目标类)的维护。
8.1.3、静态代理的实现
静态代理有一个原始类就必须有一个手工编写的代理类(源代码),每一个类都是程序员手动写的。
@Data @AllArgsConstructor @NoArgsConstructor public class User { private String username; private String password; } 复制代码
public interface UserService { void register(User user); void login(String username,String password); } 复制代码
public class UserServiceImpl implements UserService { @Override public void register(User user) { System.out.println("注册了"); } @Override public void login(String username, String password) { System.out.println("登录了"); } } 复制代码
public class UserServiceProxy implements UserService { // 原始类对象 UserServiceImpl userService = new UserServiceImpl(); @Override public void register(User user) { System.out.println("增加了日志的额外功能"); userService.register(user); } @Override public void login(String username, String password) { System.out.println("增加了登录日志的额外功能"); userService.login(username,password); } } 复制代码
8.1.4、静态代理存在的问题
- 一个原始类就有一个手动书写的代理类,会导致静态类的文件过多,不利于项目管理。
- 额外功能的维护性差,修改复杂。
8.2、Spring动态代理
既然静态代理如此的复杂和麻烦,那么Spring就帮我们搞了一个动态代理,简化了我们的开发。
8.2.1、动态代理的实现
- 创建原始对象(目标对象)
public class UserServiceImpl implements UserService { @Override public void register(User user) { System.out.println("注册了"); } @Override public void login(String username, String password) { System.out.println("登录了"); } } 复制代码
<bean id="UserServiceImpl" class="com.proxy.dynamicProxy.UserServiceImpl"/> 复制代码
- 书写额外功能
既然我们不需要手动书写代理类了,那么如何告诉Spring我们需要增加的额外方法呢?
我们需要写一个额外方法类,并且实现MethodBeforeAdvice
接口,在他规定的方法中书写额外功能
public class Before implements MethodBeforeAdvice { // 额外功能书写在接口的实现中,运行在原始方法执行之前运行额外功能 @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println("---method before advice log---"); } } 复制代码
- 去配置文件中配置
<bean id="before" class="com.proxy.dynamicProxy.Before"/> 复制代码
- 定义切入点
切入点:额外功能加入的位置,由程序员根据自己的需求,决定额外功能加入给哪个原始方法。
在测试阶段,所有方法都作为切入点,都加入额外功能。
<aop:config> <!-- 切入点,id属性是唯一标识符,expression属性是切入点表达式,这个切入点表达式的意思是所有方法都作为切入点都加入额外功能--> <aop:pointcut id="testaop" expression="execution(* *(..))"/> </aop:config> 复制代码
- 组装
组装的目的是将切入点和额外功能进行整合。
<!-- 组装:将切入点和额外功能进行整合--> <aop:advisor advice-ref="before" pointcut-ref="testaop"/> 复制代码
- 测试
利用原始对象的id
值,可以获取由Spring工厂创建的代理对象。获得代理对象后,可以声明接口类型进行对象的存储。
8.2.2、MethodBeforeAdvice
如果我们想实现动态代理,额外功能必须实现MethodBeforeAdvice
接口的before
方法,before
方法的参数解释
参数名 | 含义 |
Method method | 额外功能所增加给的那个原始方法,给谁增加额外功能就是谁,这个参数是变化的,取决于给谁增加额外方法 |
Object[] objects | 额外功能所增加给的那个原始方法的参数。给login(String uername,Stirng password)方法增加额外功能,那么这个Object数组就是对于login方法的参数列表,和上一个参数息息相关。 |
Object o | 额外功能所增加给的那个原始对象。 |
8.2.3、注意事项
- Spring创建的动态代理类在哪里?
Spring框架在运行时,通过动态字节码技术,在JVM里创建,等待程序结束后,会和JVM一起消失。
- 什么是动态字节码技术?
通过第三方动态字节码框架,直接生成JVM生成字节码,进而创建对象,当JVM结束时,动态字节码跟着消失。
- 动态代理不需要定义类文件,都是JVM运行过程中动态创建的,所以不会造成静态代理类文件数量过多、影响项目管理的问题。
- 在不改变功能的前提下,创建其他目标类(原始类)的代理对象时,只需要指定原始(目标)对象即可。
8.2.4、MethodInterceptor接口
MethodBeforeAdvice
接口的方法作用比较单一,仅仅只是可以在原始方法执行之前进行增加额外功能,Spring还提供了另一接口——MethodInterceptor
接口,他不仅仅可以在原始方法执行之前增加额外功能,还可以在原始方法执行之后增加额外功能,甚至执行前后都可以增加。
public class Arround implements MethodInterceptor { /* 书写额外功能的方法 参数:MethodInvocation表示的是额外功能所增加给的那个原始方法。 运行原始方法:methodInvocation.proceed(),在原始方法前面写的代码就运行在原始方法之前,反之。 返回值:代表原始方法返回值 */ @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { System.out.println("前置增强"); Object proceed = methodInvocation.proceed(); System.out.println("后置增强"); return proceed; } } 复制代码
8.2.5、切入点
切入点决定额外功能加入的位置。他分为两部分:
execution()
:切入点函数* *(..)
:切入点表达式
8.2.5.1、方法切入点表达式
* *(..)
:第一*
对应方法的修饰符(*
表示任意),第二个*
对应方法的方法名,(..)
对应方法的任意参数列表,所以这个切入点表达式表示的是所有方法。
定义login方法作为切入点
* login(..) 复制代码
定义login方法且方法有两个字符串类型的参数作为切入点
* login(String,String) 复制代码
这个方式有一个很致命的缺陷:切入的方法不够精准。我们需要使用精准方法切入点限定。所以我们在指定方法的时候,如果需要精准一点,需要指定包名+类名。
8.2.5.2、类切入点表达式
指定特定的类作为切入点(额外功能加入的位置),这个类中的所有方法都会加上对应的额外功能。
* com.domain.UserService.*(..) 复制代码
8.2.5.3、包切入点表达式
指定包作为额外功能加入的位置,自然包中的所有类及其方法都会加入额外的功能。在实战中运用比较多。
# 切入点包中的所有类,必须在proxy中,不能在proxy包的子包中 * com.domain.proxy.*.*(..) # 如果想要当前包及其当前包的子包都进行功能增强的话,必须要这样写 * com.domain.proxy..*.*(..) 复制代码
8.2.5.4、切入点函数
切入点函数式用于执行切入点表达式,execution
是最为重要的切入点函数,功能最全,可以执行方法切入点表达式、类切入点表达式、包切入点表达式。
他的弊端是执行切入点表达式时,书写比较麻烦。所以Spring提供了其他切入点函数来进行简化execution
书写的复杂度。
8.2.5.4.1、args
他的主要作用是用于函数(方法)的参数匹配。
# 方法参数必须是两个字符串类型的参数 args(String,String) 复制代码
8.2.5.4.2、within
主要用于类、包切入点表达式的匹配。
# 切入点想选为某个类(UserServiceImpl这个类作为切入点) whithin(*..UserServiceImpl) # 切入点想选为某个包 within(com.poroxy..*) 复制代码
8.2.5.4.3、@annotation
为具有特殊注解的方法加入额外功能,语法格式:@annotation(注解所在的包的全限定名)
// 先写一个自定义注解 @Target(ElementType.METHOD) // 表示可以加在哪里 @Retention(RetentionPolicy.RUNTIME) // 表示什么时候起作用 public @interface Log { } 复制代码
<aop:pointcut id="testaop" expression="@annotation(com.anno.Log)"/> 复制代码
@Log @Override public void register(User user) { System.out.println("注册了"); } 复制代码
8.2.5.5、切入点函数的逻辑运算
切入点函数的逻辑运算指的是整合多个切入点函数一起配合工作,可以完成更加复杂的需求。
8.2.5.5.1、与操作(and)
# 案例一:满足方法名为login且参数为两个字符串 execution (* login(..) and args(String,String)) 复制代码
注意:与操作不能用于同种类型的切入点函数。
# 案例二:满足方法名为login和register作为切入点 # 这是错误的,不可能一个方法同时叫login和register execution (* login(..)) and execution(* register(..)) 复制代码
8.2.5.5.2、或操作(or)
# 案例一:满足方法名为login或register作为切入点 execution(* login(..)) or execution(* register(..)) 复制代码
8.2.6、总结
8.3、AOP概述
AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。切面=切入点+额外功能。
AOP 是 OOP 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
他的作用是在程序运行期间,在不修改源码的情况下对方法进行功能增强,优势是可以减少重复代码,提高开发效率,并且便于维护。
8.4、名词解释
Spring 的 AOP 实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的部分进行代码编写,并通过配置的方式完成指定目标的方法增强。
- Target(目标对象):代理的目标对象。
- Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类。
- Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。
- Pointcut(切入点):所谓切入点是指我们要对哪些 。
- Joinpoint:进行拦截的定义。
- Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
- Aspect(切面):是切入点和通知(引介)的结合,简单来说就是切入点+增强方法。
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
8.5、AOP底层实现(动态代理)
8.5.1、JDK的动态代理
public class TestJDKProxy { public static void main(String[] args) { // 1. 创建原始对象 UserService userService = new UserServiceImpl(); // 2. 创建JDK动态代理 InvocationHandler handler = new InvocationHandler() { @Override // 参数一:表示代理对象 参数二:额外功能所增加给的原始方法 参数三: 表示原始方法的参数 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 原始对象的方法方法运行 Object ret = method.invoke(userService,args); System.out.println("========after proxy log========"); return ret; } }; UserService userServiceProxy = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),userService.getClass().getInterfaces(),handler); userServiceProxy.login("admin","123456"); userServiceProxy.register(null); } } 复制代码
8.5.2、CGlib的动态代理
JDK的动态代理是通过实现接口从而保证代理类和原始类的方法一致,但是如果碰到没有接口的时候呢,那么就需要使用到CGlib的动态代理了,他和JDK的动态代理最大的区别是CGlib的动态代理是通过父子继承的手段来实现代理类和原始类方法一致的。
CGlib创建动态代理的原理:父子继承关系创建代理对象,原始类作为父类,代理类作为子类,这样既可以保证二者方法的一致,同时在代理类中也可以做新的实现。
package cn.linstudy.cglibProxy.service; import cn.linstudy.cglibProxy.domain.User; /** * @Description * @Author XiaoLin * @Date 2021/2/25 18:44 */ public interface UserServiceImpl { void login(String username,String password); void register(User user); } 复制代码
package cn.linstudy.cglibProxy.proxy; import cn.linstudy.cglibProxy.domain.User; import cn.linstudy.jdkProxy.service.impl.UserServiceImpl; /** * @Description * @Author XiaoLin * @Date 2021/2/25 18:45 */ public class UserServiceProxy extends UserServiceImpl { @Override public void login(String username, String password) { System.out.println("登录了"+username+password); } @Override public void register(User user) { System.out.println("注册了"+user); } } 复制代码
public class TestCGlib { public static void main(String[] args) { // 创建原始对象 UserService userService = new UserService(); // 通过CGlib方式创建代理对象 Enhancer enhancer = new Enhancer(); enhancer.setClassLoader(userService.getClass().getClassLoader()); // 设置类加载器 enhancer.setSuperclass(userService.getClass()); // 设置父类 MethodInterceptor interceptor = new MethodInterceptor() { // 等同于InvocationHandler 的 invoke 方法 @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("=====cglib log====="); Object ret = method.invoke(userService, args); return ret; } }; enhancer.setCallback(interceptor); UserService serviceProxy = (UserService) enhancer.create(); serviceProxy.login(); serviceProxy.register(); } } 复制代码
8.5.3、总结
- JDK动态代理:Proxy.newProxyInstance(),通过接口创建代理实现类。
- CGlib动态代理:Enhancer,通过继承父类的方式创建代理类。
8.6、用注解实现AOP
public interface UserService { void register(User user); void login(String username, String password); } 复制代码
public class UserServiceImpl implements UserService { @Override public void register(User user) { System.out.println("注册了"); } @Override public void login(String username, String password) { System.out.println("登录了"); } } 复制代码
@Aspect public class MyAspect { @Around("execution(* * (..))") // 写切入点表达式 // joinPoint 表示原始方法 public Object Around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("====aspect前置增强===="); Object ret = joinPoint.proceed(); // 代表原始方法执行 System.out.println("====aspect后置增强===="); return ret; } } 复制代码
8.6.1、@Pointcut
如果我们想为多个方法配置同一个切入点表达式,那么就会出现冗余,这个时候我们能想到的就是将公共的切入点表达式提取出来,那么就需要使用到一个注解:
@Pointcut
,注意的是,注解所在的方法必须是public void修饰,且没有方法体。
切入点复用就是在切面类中定义一个函数,用@Pointcut
注解,通过这种方式,定义切入点表达式,后续更加有利于切入点的复用。
@Aspect public class MyAspect { // 将公共的切入点表达式提取出来,注意的是,注解所在的方法必须是public void修饰,且没有方法体 @Pointcut("execution(* * (..))") @Around(value = "MyPoint()") // 引入切入点表达式 public Object Around(ProceedingJoinPoint joinPoint) throws Throwable { // joinPoint 表示原始方法 System.out.println("====aspect前置增强===="); Object ret = joinPoint.proceed(); // 代表原始方法执行 System.out.println("====aspect后置增强===="); return ret; } @Around(value = "MyPoint()") // 引入切入点表达式 public Object Around1(ProceedingJoinPoint joinPoint) throws Throwable { // joinPoint 表示原始方法 System.out.println("====aspect tx===="); Object ret = joinPoint.proceed(); // 代表原始方法执行 System.out.println("====aspect tx===="); return ret; } } 复制代码
8.6.2、动态代理的创建方式
AOP底层实现有两种:
- JDK的动态代理:通过接口实现,做新的实现方法来创建代理对象。
- CGlib动态代理:通过继承父类,做一个新的子类出来创建代理对象。
在默认情况下,我们dubug一下可以发现,默认使用的是JDK动态代理的方式来进行AOP编程的。
在某些情况下,我们想将默认的JDK动态代理的方式转变为CGlib动态代理的方式,那么该如何实现呢?
在配置文件中,我们之前写过一个配置<aop:aspectj-autoproxy />
,这段配置的作用是告诉Spring,我们要开始基于注解在进行AOP编程了,这段配置中有一个属性proxy-target-class="false"
,他的默认值是false
,表示默认使用JDK的动态代理,如果将中国值设置为true
,那么则表示使用CGlib动态代理。
这个标签只适用于基于注解的AOP开发。如果是基于传统的AOP开发的话,不基于注解,那么需要在<aop-config>
标签中,添加proxy-target-class="false"
属性配置即可。和注解的方式相比,属性是一样的,只是写的位置不一样。
8.7、总结
九、注解编程
9.1、注解编程概述
注解编程指的是在类或者方法上加入特定的注解,完成特定功能的开发。使用注解的好处:
- 注解编程开发更方便,代码简洁,开发速度大大提升。
- 注解编程是Spring开发的新趋势。
9.2、注解的作用
- 简化xml文件中繁杂的配置。
- 替换接口,实现调用双方的契约性。通过注解的方式,在功能调用者和功能提供者之间达成约定,进而进行功能的调用。注解形式是主流。
9.3、对象创建相关的注解
这个阶段的注解仅仅只是为了简化XML配置而存在的,并不能完全替代XML。
9.3.1、@Component
9.3.1.1、代码示例
既然我们开始使用注解开发,那么就需要告诉Spring,让Spring框架在你设置的包及其子包中扫描对应的注解,让他生效。
<context:component-scan base-package:"com.lin" /> 复制代码
package cn.lin; import org.springframework.stereotype.Component; @Component @Data public class User { private String id; private String username; private String password; } 复制代码
<?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="cn.lin"/> </beans> 复制代码
package cn.lin; import static org.junit.Assert.*; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @Description * @Author XiaoLin * @Date 2021/2/24 21:58 */ public class UserTest { /** * 用于测试注解@Component */ @Test public void testComponent(){ ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml"); User user = app.getBean("user", User.class); System.out.println(user); } } 复制代码
9.3.1.2、注意
- 使用
@Component
注解,他创建的对象的id值默认是类名的首字母小写。 - 我们可以通过
@Component("自定义id值")
方式来给创建的对象指定一个id值。 - Spring配置文件是可以覆盖注解配置的内容。id的值和class的值需要保持一致,如果不一致的话Spring会以为这个是一个新的对象,进而不去覆盖。
9.3.1.3、@Component的衍生注解
@Component有三个衍生注解:
- @Repository
- @Service
- @Controller
这些注解本质上就是@Component注解,用法(替代标签)和细节与@Component没有任何区别,本质上是一模一样的。我们查看源码@Repository可以证明这个结论(其他同理可证)。
Spring提供这些注解是为了更准确的表达一个类型的作用。
- @Repository:主要用于Dao接口。
- @Service:主要用于Service实现类。
- @Controller:主要用于控制器。
9.3.2、@Scope
9.3.2.1、概述
@Scope
注解用于控制简单对象的创建次数。如果我们不添加@Scope
注解的话,他有一个默认值是singleton
。
9.3.2.2、代码示例
// 单例模式,只会创建一次 @Scope("singleton") public class User(){ } 复制代码
9.3.3、@Lazy
9.3.3.1、概述
@Lazy
注解的作用是延迟创建单实例对象。
如果没有进行配置的话,默认是在容器创建的时候,同步创建对象。
如果添加了注解的话,在容器创建的时候就不会同步创建对象,而是在你需要使用的时候再创建对象。
9.4、声明周期方法相关的注解
9.4.1、@PostConstruct
@PostConstruct
注解用于方法上,表示这个方法是一个初始化方法。在引入这个注解时,我们需要先加入一组依赖。
<!-- https://mvnrepository.com/artifact/javax.annotation/javax.annotation-api --> <dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>1.3.2</version> </dependency> 复制代码
@Component @Data public class User { // 表示这个是一个初始化方法 @PostConstruct public void MyInit(){ System.out.printf("User Init"); } 复制代码
9.4.2、@PreDestroy
@PreDestroy
注解用于方法上,表示这个方法是一个销毁方法。工厂关闭时会自动调用销毁方法。
@PreDestroy public void MyDestory(){ System.out.printf("User Destory"); } 复制代码
9.4.3、注意
这连个注解都不是Spring提供的,是JSR(JavaEE规范)520提供的。所以需要导入依赖。
9.5、注入相关的注解
9.5.1、用户自定义类型:@Autowired
9.5.1.1、概述
@Autowired
是一种注解,可以对成员变量、方法和构造函数进行标注,来完成自动装配的工作.
@Autowired
标注可以放在成员变量上,也可以放在成员变量的set方法上,也可以放在任意方法上表示,自动执行当前方法,如果方法有参数,会在IOC容器中自动寻找同类型参数为其传值。
9.5.1.2、代码示例
package cn.lin.dao; import cn.lin.User; /** * @Description * @Author XiaoLin * @Date 2021/2/26 9:17 */ public interface UserMapper { public void login(User user); } 复制代码
package cn.lin.dao; import cn.lin.User; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; /** * @Description * @Author XiaoLin * @Date 2021/2/26 9:18 */ @Repository public class UserMapperImpl implements UserMapper{ @Override public void login(User user) { System.out.printf("登录了"); } } 复制代码
package cn.lin.service; import cn.lin.User; /** * @Description * @Author XiaoLin * @Date 2021/2/26 9:19 */ public interface UserService { void login(User user); } 复制代码
package cn.lin.service.impl; /** * @Description * @Author XiaoLin * @Date 2021/2/26 9:19 */ @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public void login(User user) { userMapper.login(new User()); } } 复制代码
/** * 用于测试 */ @Test public void test(){ ApplicationContext app = new ClassPathXmlApplicationContext("/applicationContext.xml"); UserService userService = (UserService)app.getBean("userServiceImpl"); userService.login(new User()); } 复制代码
9.5.1.3、注意
@Autowired
注解默认是基于类型进行注入,注入对象的类型必须与目标成员变量类型相同或者是其子类、实现类。
- 要是想基于名字进行注入的话,需要
@Autowired
结合另一个注解:@Qualifier("需要注入的类的id值")
。基于名字的注入要求注入的对象的ud值必须与@Qualifier
注解中设置的名字相同。
@Autowired
注解可以放置的位置:
- 可以放置在对应成员变量的set方法上。Spring变量会调用set方法进行注入。
- 直接放置在需要注入的成员变量之上。Spring会通过反射直接对成员变量进行注入。(推荐)
十、Spring整合Mybatis
10.1、整合概述
JavaEE开发需要持久层来进行访问数据库的操作,但是现有的持久层开发过程中存在大量的代码冗余,不方便我们开发和后期维护,而Spring基于模板设计模似对于上述的持久层技术进行了封装,方便了我们的开发和减少了代码冗余。
10.2、传统Mybatis编码存在的问题
MyBatis开发步骤回顾:
- 编写实体类。
- 配置实体别名。
- 创建表。
- 创建DAO接口。
- 实现Mapper文件。
- 注册Mapper文件。
- MyBatis的API的调用。
我们在开发MyBatis的时候可以发现,传统的MyBatis的开发有一个很致命的弊端:配置繁琐且代码冗余。于是我们需要Spring这个超级工厂来进行整合。
10.3、整合思路
10.4、代码实现
MyBatis整合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="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql:///javaweb?characterEncoding=utf-8&useSSL=false"/> <property name="username" value="root"/> <property name="password" value="1101121833"/> </bean> <!-- 创建SqlSessionFactory、创建SqlSessionFactoryBean--> <bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 数据源--> <property name="dataSource" ref="dataSource"/> <!-- 别名--> <property name="typeAliasesPackage" value="com.lin.domain"/> <!-- mapper配置文件路径--> <property name="mapperLocations"> <list> <value>classpath:com/lin/mapper/*Mapper.xml</value> </list> </property> </bean> <!-- Dao对象的创建--> <bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- sqlSessionFactoryBean对象--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/> <!-- Dao接口的位置--> <property name="basePackage" value="com.lin.mapper"/> </bean> </beans> 复制代码
/** * 用于测试Spring与Mybatis整合 */ @Test public void testSpringMybatis(){ ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext( "/applicationContext.xml"); UserMapper userMapper = (UserMapper) app.getBean("userMapper"); List<User> users = userMapper.queryAll(); System.out.println(users); } 复制代码
10.5、注意
- Spring与Mybatis整合的时候,为什么不提交事物,但是我们插入数据的时候依然可以插入到数据可呢?
分析:本质上谁控制了链接对象,谁就可以提交事物。连接对象是由连接池来获取的: 1. MyBatis提供的连接池对象:他将Connection.setAutoCommit的值从默认值true设置成为了false,所以导致无法自动提交,手动控制事物,需要在操作完成后,手工提交事务。 2. Druid(C3P0、DBCP)提供的连接池对象:Connection.setAutoCommit的值为默认值true,保持自动控制事物,自动提交。 复制代码
- 在未来实际开发中,一般都是多条sql一起成功或者一起失败,我们还是需要手动提交。但是后续我们会把事物交给Spring通过事物控制来解决。
10.6、Spring的事物处理
10.6.1、事物概述
保证业务操作完整性的一种数据库机制。具有四个特性:A(原子性)、C(一致性)、I(隔离性)、D(持久性)。
常见的两种持久化技术的事物控制:
- JDBC (依赖于Connection对象来进行事物控制)
Connection.setAutoCommit(false);
Connection.commit();
Connection.rollback(); - MyBatis(自动提交事务)
sqlSession.commit();
sqlSession.rollback(); - 结论:控制事物的底层都是Connection对象来控制的(sqlSession的底层也是Connection)。
10.6.2、编码实现
<!-- 配置原始对象--> <bean id="userService" class="com.lin.service.impl.UserServiceImpl"> <property name="userMapper" ref="userMapper"/> </bean> <!-- 额外功能,Spring帮我们封装好了一个对象DataSourceTransactionManager,简化我们的开发--> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 先注入数据源,获取链接对象,进而才可以控制事物--> <property name="dataSource" ref="dataSource" /> </bean> <!-- 组装切面--> <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/> 复制代码
@Transactional // 切入点,给谁加入事物 public class UserServiceImpl implements UserService { private UserMapper userMapper; public UserMapper getUserMapper() { return userMapper; } public void setUserMapper(UserMapper userMapper) { this.userMapper = userMapper; } @Override public List<User> queryAll() { return userMapper.queryAll(); } } 复制代码
十一、事务
11.1、事务的属性
属性是描述物体特征的一系列值,事务有五个属性:
- 隔离属性
- 传播属性
- 只读属性
- 超时属性
- 异常属性
我们可以用@Transactional()
注解在添加属性,在括号被给事务添加属性。
11.2、隔离属性
隔离属性描述了事务解决并发问题的特征。事务并发访问会产生三个问题:
- 脏读
- 不可重复读、
- 幻读
我们可以通过隔离属性来解决事务并发产生的问题,在隔离属性中设置不同的值。
11.2.1、脏读
一个事务读取了另一个事务中没有提交的数据,会在本事务中产生数据不一致的问题。这个是Oracle的默认属性。
解决方案:给@Transactional()
注解添加isolation=Isolation.READ_COMMITTED
。
11.2.2、不可重复读
一个事务中,多次读取相同的数据,但是读取的结果不一样,会在本事务中产生数据不一致的问题。他读取的数据不是脏数据,只是查询的数据一致。这个是MySQL的默认属性。
解决办法:给@Transactional()
注解添加isolation=Isolation.REPEATABLE.READ
。他的本质是给该条数据加一把行级锁。
11.2.3、幻读
一个事务,多次对整表进行查询统计,但是结果的行数不一样,会在本事务中产生数据不一致的问题。
解决办法:给@Transactional()
注解添加isolation=Isolation.SERIALIZABLE
。他的本质是给整个表加一把表锁。
11.2.4、总结
并发安全排序:Isolation.SERIALIZABLE>Isolation.REPEATABLE.READ>Isolation.READ_COMMITTED
性能排序:Isolation.SERIALIZABLE<Isolation.REPEATABLE.READ<Isolation.READ_COMMITTED
# 我们可以用命令来查询MySQL的默认属性。 select @@tx_isolation 复制代码
隔离属性的值 | MySQL | Oracle |
ISOLATION_READ_COMMITTED | ✅ | ✅ |
IOSLATION_REPEATABLE_READ | ✅ | ❎ |
ISOLATION_SERIALIZABLE | ✅ | ✅ |
11.3、传播属性
传播属性描述了事务解决嵌套问题的特征。
事务嵌套:一个大的事务里面包含着几个小的事务。
事务嵌套由于大事务中融入了很多的小事务,他们彼此影响,最终就会导致外部的大事务丧失了事务的原子性。我们用传播属性的值来解决这个问题,传播属性的值保证了在同一时间只会有一个事务存在。
11.3.1、事务传播属性详解
融合:自己的事务没有了,以外部的事务为准。
挂起:相当于暂停。
传播属性的值 | 外部不存在事务 | 外部存在事务 | 用法 | 使用场景 |
REQUIRED**(默认)** | 开启新的事务 | 融合到外部事务中 | @Transactional(propagation = Propagation.REQUIRED) | 增删改方法 |
SUPPORTS | 不开启事务 | 融合到外部事务中 | @Transactional(propagation = Propagation.SUPPORTS) | 查询方法 |
REQUIRES_NEW | 开启新的事务 | 挂起外部事务,创建新的事务。 | @Transactional(propagation = Propagation.REQUIRES_NEW) | 日志记录方法中 |
NOT_SUPPORTED | 不开启事务 | 挂起外部事务 | @Transactional(propagation = Propagation.NOT_SUPPORTED) | 及其不常用 |
NEVER | 不开启事务 | 抛出异常 | @Transactional(propagation = Propagation.NEVER) | 及其不常用 |
MANDATORY | 抛出异常 | 融合到外部事务中 | @Transactional(propagation = Propagation.MANDATORY) | 及其不常用 |
11.4、只读属性
针对只进行查询操作的业务方法,可以加入只读属性(readlyonly = true
),提高运行的效率。他的默认值为false。
11.5、超时属性
当前的事务访问数据时,有可能访问的数据被别的事务进行加锁处理,那么此时本事务就需要进行等待。超时属性指定了事务等待的最长时间,这个时间是以秒为单位的。他的默认值是-1,他最终的时间由对应的数据库来决定。
应用示范:@Transaction(timeout=秒数)
;
11.6、异常属性
Spring事务处理过程中,默认对RuntimeException及其子类采用的是回滚策略,而对于Exception及其子类采用的是提交策略。
// 异常属性有两个值 // 1. rollbackFor();表示回滚的异常类型,里面写异常的类型的字节码对象,他里面的值的类型是数组类型,所以可以写多个异常类型的字节码对象,用逗号隔开 rollbackFor({java.lang.Exception.class}); // noRollbackFor();表示不会滚的异常类型的字节码对象 noRollbackFor({java.lang.RuntimeException.class}) 复制代码
11.7、事务属性及其常见配置总结
- 隔离属性:一般用默认值。
- 传播属性:一般增删改用Required(默认值),查询操作用Supports。
- 只读属性:一般增删改设置readOnly的值为false,查询设置readOnly的值为true。
- 超时属性:一般用默认值为-1。
- 异常属性:一般用默认值。 总结:
- 增删改操作:
@Transactional
。 - 查询操作:
@Transactional(propagation=Propagation.SUPPORTS,readOnly=true)
十二、Spring整合MVC
12.1、依赖准备
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.1.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.1.5.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.1.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.1.5.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.2</version> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.13</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.8</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.3</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.25</version> </dependency> <!-- https://mvnrepository.com/artifact/log4j/log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!-- https://mvnrepository.com/artifact/javax.servlet/jstl --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.1</version> <scope>provided</scope> </dependency> </dependencies> 复制代码
12.2、为什么要整合MVC框架
因为有很多的需求和功能是原生的Spring框架无法解决的,需要MVC框架来简化我们的开发。常用的MVC框架有:SpringMVC、Struts2、Struts1、jsf、webwork(除了第一个其他的都被淘汰了)。
- MVC框架提供了控制器(Controller),用来调用Service层。
- 请求响应的处理。
- 接受请求参数。
- 控制程序的运行流程。
- 视图解析(JSP、JSON、Freemarker、Thyemeleaf)
12.3、Spring整合MVC框架的核心思路
准备工厂
1. Web开发过程中如何创建工厂 ApplicationContext ctx = new WebXmlApplication("/applicationContext.xml"); 2. 如何保证工厂被共用同时保证唯一性 被共用:在Web中有四个作用于:request、session、ServletContext(application),我们可以把工厂存储到ServletContext这个作用域中。 唯一性:我们可以利用ServletContextListener这个监听器,因为在ServletContext对象创建的同时,ServletContextListener会被调用且只被调用一次。所以我们可以把工厂创建的代码写在ServletContextListener中,这样就保证了唯一性。 3. 总结 我们首先在ServletContextListener这个监听器中书写创建工厂的代码,其次将这个工厂放在ServletContext对象中。 4. Spring的封装 既然这些代码如此繁琐,那么Spring肯定会帮我们封装好了。 5. ContextLoaderListener类 Spring封装了一个ContextLoaderListener类,他的作用是创建工厂和将工厂放进ServletContext对象中。 复制代码
配置文件
既然Spring帮我们封装好了一个ContextLoaderListener类,那么我们直接按照监听器在web.xml中进行配置即可。
<!---我们只需要在web.xml中进行配置即可--> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!--告诉Spring我们的配置文件在哪里--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> 复制代码
代码整合
代码整合的核心是依赖注入,将控制器需要的Service对象注入到控制器对象中。