Spring-03
1. AOP
1.1 概念
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程。他是一种可以在不修改原来的核心代码的情况下给程序动态统一进行增强的一种技术。
SpringAOP: 批量对Spring容器中bean的方法做增强,并且这种增强不会与原来方法中的代码耦合。
1.2 快速入门
1.2.1 需求
要求让_08_SpringAOP模块中service包下所有类的所有方法在调用前都输出:方法被调用了。
按照之前的方式,我们在每个方法里面加入输出语句,缺点是增强代码与核心代码直接耦合。进行修改时非常麻烦。
1.2.2 准备工作
①添加依赖
需要添加SpringIOC相关依赖和AOP相关依赖。
<!--SpringIOC相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<!--AOP相关依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
②相关bean要注入容器中
开启组件扫描
<context:component-scan base-package="com.sangeng"></context:component-scan>
加@Service注解
@Service
public class PhoneService {
public void deleteAll(){
System.out.println("PhoneService中deleteAll的核心代码");
}
}
@Service
public class UserService {
public void deleteAll(){
System.out.println("UserService中deleteAll的核心代码");
}
}
1.2.3 实现AOP
①开启AOP注解支持
使用aop:aspectj-autoproxy标签
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
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/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--开启组件扫描-->
<context:component-scan base-package="com.sangeng"></context:component-scan>
<!--开启aop注解支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
②创建切面类
创建一个类,在类上加上@Component和@Aspect
使用@Pointcut注解来指定要被增强的方法
使用@Before注解来给我们的增强代码所在的方法进行标识,并且指定了增强代码是在被增强方法执行之前执行的。
@Component
@Aspect
public class MyAspect {
// 用Pointcut注解中的属性来指定对哪些方法进行增强
@Pointcut("execution(* com.sangeng.service.*.*(..))")
public void pt(){}
/*
用@Before注解来指定该方法中是增强的代码,并且是在被增强方法执行前执行的
@Before的属性写上加了@Pointcut注解的方法: 方法名()
*/
@Before("pt()")
public void methodbefore(){
System.out.println("方法被调用了");
}
}
1.2.4 测试
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
PhoneService phoneService = applicationContext.getBean(PhoneService.class);
UserService userService = applicationContext.getBean(UserService.class);
phoneService.deleteAll();
}
不想增强直接去掉那两个注解即可(在这样就不会被放到spring容器里)
1.3 AOP核心概念
-
Joinpoint(连接点):所谓连接点是指那些可以被增强到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
-
Pointcut(切入点):所谓切入点是指被增强的连接点(方法)
-
Advice(通知/ 增强):所谓通知是指具体增强的代码 上图的methodbefore方法
-
Target(目标对象):被增强的对象就是目标对象
-
Aspect(切面):是切入点和通知(引介)的结合 上图两个的结合
-
Proxy (代理):一个类被 AOP 增强后,就产生一个结果代理类。意味着,spring容器中最终得到的不是phoneservice这个类的对象,而是他增强后的对象。
如果不用AOP的话
1.4 切点确定
1.4.1 切点表达式
可以使用切点表达式来表示要对哪些方法进行增强。
写法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
- 访问修饰符可以省略,大部分情况下省略
- 返回值类型、包名、类名、方法名可以使用星号* 代表任意
- 包名与类名之间一个点 . 代表当前包下的类,两个点 … 表示当前包及其子包下的类
- 参数列表可以使用两个点 … 表示任意个数,任意类型的参数列表
例如:
execution(* com.sangeng.service.*.*(..)) 表示com.sangeng.service包下任意类,方法名任意,参数列表任意,返回值类型任意
execution(* com.sangeng.service..*.*(..)) 表示com.sangeng.service包及其子包下任意类,方法名任意,参数列表任意,返回值类型任意
execution(* com.sangeng.service.*.*()) 表示com.sangeng.service包下任意类,方法名任意,要求方法不能有参数,返回值类型任意
execution(* com.sangeng.service.*.delete*(..)) 表示com.sangeng.service包下任意类,要求方法不能有参数,返回值类型任意,方法名要求已delete开头
1.4.2 切点函数@annotation(用到的更多,因为更加灵活)
我们也可以在要增强的方法上加上注解。然后使用@annotation来表示对加了什么注解的方法进行增强。
写法:@annotation(注解的全类名)
例如:
自定义注解如下(如果忘了,可以随便输一个注解点进去看看此注解是如何定义的即可)
@Target({ElementType.METHOD}) //该注解可以加在方法上
@Retention(RetentionPolicy.RUNTIME) //该注解可以保持到什么时期
public @interface InvokeLog {
}
给需要增强的方法增加注解
@Service
public class PhoneService {
@InvokeLog
public void deleteAll(){
System.out.println("PhoneService中deleteAll的核心代码");
}
}
切面类中使用@annotation来确定要增强的方法
@Component
@Aspect
public class MyAspect {
// 用Pointcut注解中的属性来指定对哪些方法进行增强
@Pointcut("@annotation(com.sangeng.aspect.InvokeLog)")
public void pt(){}
/*
用@Before注解来指定该方法中是增强的代码,并且是在被增强方法执行前执行的
@Before的属性写上加了@Pointcut注解的方法: 方法名()
*/
@Before("pt()")
public void methodbefore(){
System.out.println("方法被调用了");
}
}
1.5 通知分类
-
@Before:前置通知,在目标方法执行前执行
-
@AfterReturning: 返回后通知,在目标方法执行后执行,如果出现异常不会执行
-
@After:后置通知,在目标方法之后执行,无论是否出现异常都会执行
-
@AfterThrowing:异常通知,在目标方法抛出异常后执行
-
@Around:环绕通知,围绕着目标方法执行
理解不同通知执行时机。(下面的伪代码是用来理解单个通知的执行时机的,不能用来理解多个通知情况下的执行顺序。如果需要配置多个通知我们会选择使用Around通知,更加的清晰并且好用)
public Object test() {
before();//@Before 前置通知
try {
Object ret = 目标方法();//目标方法调用
afterReturing();//@AfterReturning 返回后通知
} catch (Throwable throwable) {
throwable.printStackTrace();
afterThrowing();//@AfterThrowing 异常通知通知
}finally {
after();//@After 后置通知
}
return ret;
}
环绕通知非常特殊,它可以对目标方法进行全方位的增强。
例如:
@Around("pt()")
public void around(ProceedingJoinPoint pjp){
System.out.println("目标方法前");
try {
pjp.proceed();//目标方法执行
System.out.println("目标方法后");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("目标方法出现异常");
}finally {
System.out.println("finally中进行增强");
}
}
1.6 获取被增强方法相关信息
我们实际对方法进行增强时往往还需要获取到被增强代码的相关信息,比如方法名,参数,返回值,异常对象等。
我们可以在除了环绕通知外的所有通知方法中增加一个JoinPoint类型的参数。这个参数封装了被增强方法的相关信息。我们可以通过这个参数获取到除了异常对象和返回值之外的所有信息。
例如:
@Before("pt()")
public void methodbefore(JoinPoint jp){
Object[] args = jp.getArgs();//方法调用时传入的参数
Object target = jp.getTarget();//被代理对象
MethodSignature signature = (MethodSignature) jp.getSignature();//获取被被增强方法签名封装的对象
System.out.println("Before方法被调用了");
}
忘掉了的话可以下断点,找小计算器
最重要的getSignature(),与下文案例内容一致
案例:
需求:要求让所有service包下类的所有方法被调用前都输出全类名,方法名,以及调用时传入的参数
@Component
@Aspect
public class PrintLogAspect {
//对哪些方法增强
@Pointcut("execution(* com.sangeng.service..*.*(..))")
public void pt(){}
//怎么增强
@Before("pt()")
public void printLog(JoinPoint joinPoint){
//输出 被增强的方法所在的类名 方法名 调用时传入的参数 joinPoint.getSignature().getName() joinPoint.getArgs()
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//类名
String className = signature.getDeclaringTypeName();
//方法名
String methodName = signature.getName();
//调用时传入的参数
Object[] args = joinPoint.getArgs();
System.out.println(className+"=="+methodName+"======"+ Arrays.toString(args));
}
}
如果需要获取被增强方法中的异常对象或者返回值则需要在方法参数上增加一个对应类型的参数,并且使用注解的属性进行配置。这样Spring会把你想获取的数据赋值给对应的方法参数。
例如:
@AfterReturning(value = "pt()",returning = "ret")//使用returning属性指定了把目标方法返回值赋值给下面方法的参数ret
public void AfterReturning(JoinPoint jp,Object ret){
System.out.println("AfterReturning方法被调用了");
}
@AfterThrowing(value = "pt()",throwing = "t")//使用throwing属性指定了把出现的异常对象赋值给下面方法的参数t
public void AfterThrowing(JoinPoint jp,Throwable t){
System.out.println("AfterReturning方法被调用了");
}
相信你肯定觉得上面的获取方式特别的麻烦难以理解。就可以使用下面这种万能的方法。(更常用)
直接在环绕通知方法中增加一个ProceedingJoinPoint类型的参数。这个参数封装了被增强方法的相关信息。
该参数的proceed()方法被调用相当于被增强方法被执行,调用后的返回值就相当于被增强方法的返回值。
例如:
@Around(value = "pt()")
public Object around(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();//方法调用时传入的参数
Object target = pjp.getTarget();//被代理对象
MethodSignature signature = (MethodSignature) pjp.getSignature();//获取被增强方法签名封装的对象
Object ret = null;
try {
ret = pjp.proceed();//ret就是目标方法执行后的返回值
} catch (Throwable throwable) {
throwable.printStackTrace();//throwable就是出现异常时的异常对象
}
return ret;
}
一些注意点:
目标方法可能有返回值,案例中切的是所有service包下的方法,有些方法是有返回值的
这种情况下,一定要咋子切面类对应的方法当中,一定要有返回值
确保ret的作用范围不是只在try里面
我们也3可以篡改目标方法的返回值,上面是返回目标方法的返回值。下面则是篡改。
1.7 AOP应用案例
1.7.1 需求
现有AI核心功能代码如下:
public class AIController {
//AI自动回答
public String getAnswer(String question){
//AI核心代码 价值10个亿
String str = question.replace("吗", "");
str = str.replace("?","!");
return str;
}
//AI算命
public String fortuneTelling(String name){
//AI算命核心代码
String[] strs = {"女犯伤官把夫克,旱地莲花栽不活,不是吃上两家饭,也要刷上三家锅。","一朵鲜花头上戴,一年四季也不开,一心想要花开时,采花之人没到来。","此命生来脾气暴,上来一阵双脚跳,对你脾气啥都好,经常与人吵和闹。"};
int index = name.hashCode() % 3;
return strs[index];
}
}
现在为了保证数据的安全性,要求调用方法时fortuneTelling传入的姓名是经过加密的。我们需要对传入的参数进行解密后才能使用。并且要对该方法的返回值进行加密后返回。
PS:后期也可能让其他方法进行相应的加密处理。
在每个方法中单独写加解密,很繁琐,因为代码一样。
在不需要加解密的时候,又得删改,耦合。
所以使用AOP来完成
1.7.2 实现
①导入依赖
<!--SpringIOC相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<!--AOP相关依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
②开启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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置组件扫描-->
<context:component-scan base-package="com.sangeng"></context:component-scan>
<!--启动AOP注解支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
③自定义注解
因为用注解,只需要在需要增强的方法上面加上注解进行标识,后期切面类的切点表达式根本不需要修改。
package com.sangeng.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Crypt {
}
④在目标方法上增加注解
注意:目标对象一定要记得注入Spring容器中
@Controller
public class AIController {
//....
//AI算命
@Crypt
public String fortuneTelling(String name){
System.out.println(name);
//AI算命核心代码
String[] strs = {"女犯伤官把夫克,旱地莲花栽不活,不是吃上两家饭,也要刷上三家锅。","一朵鲜花头上戴,一年四季也不开,一心想要花开时,采花之人没到来。","此命生来脾气暴,上来一阵双脚跳,对你脾气啥都好,经常与人吵和闹。"};
int index = name.hashCode() % 3;
return strs[index];
}
}
⑤定义切面类
因为是把加密后的字符串传到方法当中,所以在目标方法之前要先解密。
同时,返回的字符串是明文,所以我们还需要在目标方法结束之后加密才行。
显然选择@Around是最好的
package com.sangeng.aspect;
import com.sangeng.util.CryptUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class CryptAspect {
//确定切点
@Pointcut("@annotation(com.sangeng.aspect.Crypt)")
public void pt(){
}
//定义通知
@Around("pt()")
public Object crypt(ProceedingJoinPoint pjp) {
//获取去目标方法调用时的参数
Object[] args = pjp.getArgs();
//对参数进行解密 解密后传入目标方法执行
String arg = (String) args[0];
String s = CryptUtil.AESdecode(arg);//解密
args[0] = s;
Object proceed = null;
String ret = null;
try {
proceed = pjp.proceed(args);//目标方法调用
//目标方法执行后需要获取到返回值
ret = (String) proceed;
//对返回值加密后进行真正的返回
ret = CryptUtil.AESencode(ret);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return ret;
}
}
1.7.3 测试时的步骤(一定要保证边写边测)
- 拿到这个需求和代码后,第一步先进行测试。
我们把这个类的对象放到spring容器中去进行管理,从容器当中获取他,然后调用它的方法进行测试。
- 测试加解密工具(网上一堆库)
完成上面的所有代码后demo测试
1.8 xml配置AOP(可以略过,因为注解更流行)
①定义切面类
public class MyAspect {
public void before(JoinPoint joinPoint){
System.out.println("before");
}
// @AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(JoinPoint joinPoint,Object ret){
System.out.println("afterReturning:"+ret);
}
// @After("pt()")
public void after(JoinPoint joinPoint){
System.out.println("after");
}
// @AfterThrowing(value = "pt()",throwing = "e")
public void afterThrowing(JoinPoint joinPoint,Throwable e){
String message = e.getMessage();
System.out.println("afterThrowing:"+message);
}
public Object around(ProceedingJoinPoint pjp){
//获取参数
Object[] args = pjp.getArgs();
MethodSignature signature = (MethodSignature) pjp.getSignature();
Object target = pjp.getTarget();
Object ret = null;
try {
ret = pjp.proceed();//目标方法的执行
//ret就是被增强方法的返回值
System.out.println(ret);
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println(throwable.getMessage());
}
// System.out.println(pjp);
return ret;
}
}
②目标类和切面类注入容器
在切面类和目标类上加是对应的注解。注入如果是使用注解的方式注入容器要记得开启组件扫描。
当然你也可以在xml中使用bean标签的方式注入容器。
@Component//把切面类注入容器
public class MyAspect {
//..。省略无关代码
}
@Service//把目标类注入容器
public class UserService {
//..。省略无关代码
}
③配置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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启组件扫描-->
<context:component-scan base-package="com.sangeng"></context:component-scan>
<!--配置AOP-->
<aop:config>
<!--定义切点-->
<aop:pointcut id="pt1" expression="execution(* com.sangeng.service..*.*(..))"></aop:pointcut>
<aop:pointcut id="pt2" expression="@annotation(com.sangeng.aspect.InvokeLog)"></aop:pointcut>
<!--配置切面-->
<aop:aspect ref="myAspect">
<aop:before method="before" pointcut-ref="pt1"></aop:before>
<aop:after method="after" pointcut-ref="pt1"></aop:after>
<aop:after-returning method="afterReturning" pointcut-ref="pt1" returning="ret"></aop:after-returning>
<aop:after-throwing method="afterThrowing" pointcut-ref="pt2" throwing="e"></aop:after-throwing>
</aop:aspect>
</aop:config>
</beans>
1.9 多切面顺序问题
在实际项目中我们可能会存在配置了多个切面的情况。这种情况下我们很可能需要控制切面的顺序。
我们在默认情况下Spring有它自己的排序规则。(按照类名排序)
默认排序规则往往不符合我们的要求,我们需要进行特殊控制。
如果是注解方式配置的AOP可以在切面类上加**@Order注解**来控制顺序。
@Order中的属性越小优先级越高。
如果是XML方式配置的AOP,可以通过调整配置顺序来控制。
例如:
下面这种配置方式就会先使用CryptAspect里面的增强,在使用APrintLogAspect里的增强
@Component
@Aspect
@Order(2)
public class APrintLogAspect {
//省略无关代码
}
@Component
@Aspect
@Order(1)
public class CryptAspect {
//省略无关代码
}
1.10 AOP原理-动态代理
实际上Spring的AOP其实底层就是使用动态代理来完成的。并且使用了两种动态代理分别是JDK的动态代理和Cglib动态代理。
所以我们接下去来学习下这两种动态代理,理解下它们的不同点。
1.10.1 JDK动态代理
JDK的动态代理使用的java.lang.reflect.Proxy这个类来进行实现的。要求被代理(被增强)的类需要实现了接口。并且JDK动态代理也只能对接口中的方法进行增强。
public static void main(String[] args) {
AIControllerImpl aiController = new AIControllerImpl();
//使用动态代理增强getAnswer方法
//1.JDK动态代理
//获取类加载器
ClassLoader cl = Demo.class.getClassLoader();
//被代理类所实现接口的字节码对象数组
Class<?>[] interfaces = AIControllerImpl.class.getInterfaces();
AIController proxy = (AIController) Proxy.newProxyInstance(cl, interfaces, new InvocationHandler() {
//使用代理对象的方法时 会调用到invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//proxy 是代理对象
//method 是当前被调用的方法封装的Method对象
//args 是调用方法时传入的参数
//调用被代理对象的对应方法
//判断 当前调用的是否是getAnswer方法
if(method.getName().equals("getAnswer")){
System.out.println("增强");
}
Object ret = method.invoke(aiController, args);
return ret;
}
});
String answer = proxy.getAnswer("三连了吗?");
System.out.println(answer);
}
1.10.2 Cglib动态代理
使用的是org.springframework.cglib.proxy.Enhancer类进行实现的。
public class CglibDemo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
//设置父类的字节码对象
enhancer.setSuperclass(AIControllerImpl.class);
enhancer.setCallback(new MethodInterceptor() {
//使用代理对象执行方法是都会调用到intercept方法
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//判断当前调用的方法是不是getAnswer方法 如果是进行增强
if ("getAnswer".equals(method.getName())){
System.out.println("被增强了");
}
//调用父类中对应的方法
Object ret = methodProxy.invokeSuper(o, objects);
return ret;
}
});
//生成代理对象
AIControllerImpl proxy = (AIControllerImpl) enhancer.create();
// System.out.println(proxy.getAnswer("你好吗?"));
System.out.println(proxy.fortuneTelling("你好吗?"));
}
}
1.10.3 总结
JDK动态代理要求被代理(被增强)的类必须要实现接口,生成的代理对象相当于是被代理对象的兄弟。
Cglib的动态代理不要求被代理(被增强)的类要实现接口,生成的代理对象相当于被代理对象的子类对象。
Spring的AOP默认情况下优先使用的是JDK的动态代理,如果使用不了JDK的动态代理才会使用Cglib的动态代理。
1.11 切换默认动态代理方式
有的时候我们需要修改AOP的代理方式。
或者
我们可以使用以下方式修改:父子关系的话就可以
如果我们是采用注解方式配置AOP的话:
设置aop:aspectj-autoproxy标签的proxy-target-class属性为true,代理方式就会修改成Cglib
<aop:aspectj-autoproxy proxy-target-class="true"/>
如果我们是采用xml方式配置AOP的话:
设置aop:config标签的proxy-target-class属性为true,代理方式就会修改成Cglib
<aop:config proxy-target-class="true">
</aop:config>
Spring-04
1.Spring整合Junit
之所以整合是因为,我们用spring的时候,会把相关的bean放入到容器当中,测试的时候用bean来测试。
我们需要在junit当中从spring容器里面获取到对应的bean,然后拿过来测试。
①导入依赖
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- spring整合junit的依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
② 编写测试类
在测试类上加上
**@RunWith(SpringJUnit4ClassRunner.class)**注解,指定让测试运行于Spring环境
@ContextConfiguration注解,指定Spring容器创建需要的配置文件或者配置类
@RunWith(SpringJUnit4ClassRunner.class)//让测试运行与Spring测试环境
@ContextConfiguration(locations = "classpath:配置文件1.xml")//设置Spring配置文件或者配置类
//@ContextConfiguration(classes = SpringConfig.class)
public class SpringTest {}
③注入对象进行测试
在测试类中注入要测试的对象,定义测试方法,在其中使用要测试的对象。
@RunWith(SpringJUnit4ClassRunner.class)//让测试运行与Spring测试环境
@ContextConfiguration(locations = "classpath:配置文件1.xml")//设置Spring配置文件或者配置类
//@ContextConfiguration(classes = SpringConfig.class)
public class SpringTest {
// 想测哪个对象,就注入哪个对象
@Autowired
private UserService userService;
//定义测试方法
@Test
public void testUserService() {
userService.findById(10);
}
}
不要忘了IOC的知识:
userDao的数据类型是UserDao接口类型,我们希望从spring容器当中获取到对应类型的对象,完成赋值。而完成这个的就是@AutoWired
Spring会给加了该注解的属性自动注入数据类型相同的对象。
前提:类和对象都要在spring容器里面
2.Spring整合Mybatis
我们如果想把Mybatis整合到Spring中需要使用一个整合包mybatis-spring
官方文档:http://mybatis.org/spring/zh/index.html
①导入依赖
<!-- spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<!-- mybatis整合到Spring的整合包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.4</version>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.4</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!-- druid数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
②往容器中注入整合相关对象
<!--读取properties文件-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!--创建连接池注入容器-->
<bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="driverClassName" value="${jdbc.driver}"></property>
</bean>
<!--spring整合mybatis后控制的创建获取SqlSessionFactory的对象-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sessionFactory">
<!--配置连接池-->
<property name="dataSource" ref="dataSource"></property>
<!--配置mybatis配置文件的路径-->
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
</bean>
<!--mapper扫描配置,扫描到的mapper对象会被注入Spring容器中,扫描mapper-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" id="mapperScannerConfigurer">
<property name="basePackage" value="com.sangeng.dao"></property>
</bean>
mybatis配置文件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>
<typeAliases>
<package name="com.sangeng.domain"></package>
</typeAliases>
</configuration>
③从容器中获取Mapper对象进行使用
@Autowired
private UserDao userDao;
对应的用处
mybatis的方法设置(mapper)
最终测试
mybatis-spring整合配置中最为核心的部分
整合的配置完成后,只要定义完userDao接口,定义完xml映射文件(里面有sql语法),然后在service实现类注入对应的dao,就可以使用它们了
使用
3.Spring声明式事务
3.1 事务回顾
3.1.1 事务的概念
保证一组数据库的操作,要么同时成功,要么同时失败
3.1.2 四大特性
-
隔离性
多个事务之间要相互隔离,不能互相干扰
-
原子性
指事务是一个不可分割的整体,类似一个不可分割的原子
-
一致性
保障事务前后这组数据的状态是一致的。要么都是成功的,要么都是失败的。
-
持久性
指事务一旦被提交,这组操作修改的数据就真的的发生变化了。即使接下来数据库故障也不应该对其有影响。
3.2 实现声明式事务
如果我们自己去对事务进行控制的话我们就需要在原来核心代码的基础上加上事务控制相关的代码。而在我们的实际开发中这种事务控制的操作也是非常常见的。所以Spring提供了声明式事务的方式让我们去控制事务。
只要简单的加个注解(或者是xml配置)就可以实现事务控制,不需要事务控制的时候只需要去掉相应的注解即可。
3.2.0 案例环境准备
①数据初始化
CREATE DATABASE /*!32312 IF NOT EXISTS*/`spring_db` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `spring_db`;
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) DEFAULT NULL,
`money` DOUBLE DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `account`(`id`,`name`,`money`) VALUES (1,'三更',100),(2,'草堂',100);
②Spring整合Mybatis
之前的内容
③创建Service和Dao
首先是写出domain包下account类,存的数据库对应的变量
因为是转账的业务–放到service,service调用dao,dao里面应该包括两个操作,A用户钱减少,B用户钱增加
public interface AccountService {
/**
* 转账
* @param outId 转出账户的id
* @param inId 转出账户的id
* @param money 转账金额
*/
public void transfer(Integer outId,Integer inId,Double money);
}
transfer涉及到对数据库的操作,所以我们创建dao接口
public interface AccoutDao {
void updateMoney(@Param("id") Integer id,@Param("updateMoney") Double updateMoney);
}
创建对应的xml映射文件,不过在此之前先在resources下创建文件夹
AccoutDao.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.sangeng.dao.AccoutDao">
<update id="updateMoney">
update account set money = money + #{updateMoney} where id = #{id}
</update>
</mapper>
最后用注解将accountDao放入容器
写方法
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccoutDao accoutDao;
public void transfer(Integer outId, Integer inId, Double money) {
//增加
accoutDao.updateMoney(inId,money);
//减少
accoutDao.updateMoney(outId,-money);
}
}
最终完成测试
3.2.1 注解实现(最重要)
但是如果转账的时候出现了异常(用自己加入的来代替)。此时就会出现一个人钱1增加了,一个人没减
①配置事务管理器和事务注解驱动
包在jdbcc内,故不用在加依赖
事务控制加在service层
在spring的配置文件中添加如下配置:
<!--把事务管理器注入Spring容器,需要配置一个连接池-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--开启事务注解驱动,配置使用的事务管理器-->
<tx:annotation-driven transaction-manager="txManager"/>
②添加注解
在需要进行事务控制的方法或者类上添加@Transactional注解就可以实现事务控制。出现异常直接回滚
@Transactional
public void transfer(Integer outId, Integer inId, Double money) {
//增加
accoutDao.updateMoney(inId,money);
// System.out.println(1/0);
//减少
accoutDao.updateMoney(outId,-money);
}
注意:如果加在类上,这个类的所有方法都会受事务控制,如果加在方法上,就是那一个方法受事务控制。
注意,因为声明式事务底层是通过AOP实现的,所以最好把AOP相关依赖都加上。
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
3.2.2 xml方式实现(基本不用看)
①配置事务管理器
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
②配置事务切面
<!--定义事务管理的通知类-->
<tx:advice transaction-manager="txManager" id="txAdvice">
<tx:attributes>
<tx:method name="trans*"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="pt" expression="execution(* com.sangeng.service..*.*(..))"></aop:pointcut>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"></aop:advisor>
</aop:config>
注意,因为声明式事务底层是通过AOP实现的,所以最好把AOP相关依赖都加上。
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
3.3 属性配置
3.3.1 事务传播行为propagation
当事务方法嵌套调用时,需要控制是否开启新事务,可以使用事务传播行为来控制。
问题:转账异常,打印日志出现异常,结果全给回滚了.
测试案例:
@Service
public class TestServiceImpl {
@Autowired
AccountService accountService;
@Transactional
public void test(){
accountService.transfer(1,2,10D);
accountService.log();
}
}
public class AccountServiceImpl implements AccountService {
//...省略其他不相关代码
@Transactional
public void log() {
System.out.println("打印日志");
int i = 1/0;
}
}
属性值 | 行为 |
---|---|
REQUIRED(必须要有) | 外层方法有事务,内层方法就加入。外层没有,内层就新建 |
REQUIRES_NEW(必须要有新事务) | 外层方法有事务,内层方法新建。外层没有,内层也新建 |
SUPPORTS(支持有) | 外层方法有事务,内层方法就加入。外层没有,内层就也没有 |
NOT_SUPPORTED(支持没有) | 外层方法有事务,内层方法没有。外层没有,内层也没有 |
MANDATORY(强制要求外层有) | 外层方法有事务,内层方法加入。外层没有。内层就报错 |
NEVER(绝不允许有) | 外层方法有事务,内层方法就报错。外层没有。内层就也没有 |
例如:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void transfer(Integer outId, Integer inId, Double money) {
//增加
accoutDao.updateMoney(inId,money);
//减少
accoutDao.updateMoney(outId,-money);
}
3.3.2 隔离级别isolation
Isolation.DEFAULT 使用数据库默认隔离级别
Isolation.READ_UNCOMMITTED
Isolation.READ_COMMITTED
Isolation.REPEATABLE_READ
Isolation.SERIALIZABLE
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED)
public void transfer(Integer outId, Integer inId, Double money) {
//增加
accoutDao.updateMoney(inId,money);
//减少
accoutDao.updateMoney(outId,-money);
}
3.3.3 只读readOnly
如果事务中的操作都是读操作,没涉及到对数据的写操作可以设置readOnly为true。这样可以提高效率。
@Transactional(readOnly = true)
public void log() {
System.out.println("打印日志");
int i = 1/0;
}