第十七章 反射、注解与Spring事务底层原理
文章目录
一、反射
1.简介
反射这一概念最早由编程开发人员Smith在1982年提出,主要指应用程序访问、检测、修改自身状态与行为的能力.这一概念的提出立刻吸引了编程界的极大关注,各种研究工作随之展开,随之而来引发编程革命,出现了多种支持反射机制的面向对象语言
在计算机科学领域,反射是指一类能够自我描述和自控制的应用。在Java编程语言中,反射是一种强有力的工具,是面向抽象编程一种实现方式,它能使代码语句更加灵活,极大提高代码的运行时装配能力。使用反射机制可以动态获取当前 class 的信息,比如方法的信息、注解信息、方法的参数和属性等
2.意义
- 反射机制极大的提高了程序的灵活性和扩展性,降低模块的耦合性,提高自身的适应能力
- 通过反射机制可以让程序创建和控制任何类的对象,无需提前硬编码目标类
- 使用反射机制能够在运行时构造一个类的对象、判断一个类所具有的成员变量和方法、调用一个对象的方法
- 反射机制是构建框架技术的基础所在,使用反射可以避免将代码写死在框架中
正是反射有以上的特征,所以它能动态编译和创建对象,极大的激发了编程语言的灵活性,强化了多态的特性,进一步提升了面向对象编程的抽象能力,因而受到编程界的青睐
3.缺点
-
性能问题
Java反射机制中包含了一些动态类型,所以Java虚拟机不能够对这些动态代码进行优化。因此,反射操作的效率要比正常操作效率低很多。我们应该避免在对性能要求很高的程序或经常被执行的代码中使用反射。而且,如何使用反射决定了性能的高低。如果它作为程序中较少运行的部分,性能将不会成为一个问题 -
安全限制
使用反射通常需要程序的运行没有安全方面的限制。如果一个程序对安全性提出要求,则最好不要使用反射 -
程序健壮性
反射允许代码执行一些通常不被允许的操作,所以使用反射有可能会导致意想不到的后果。反射代码破坏了Java程序结构的抽象性,所以当程序运行的平台发生变化的时候,由于抽象的逻辑结构不能被识别,代码产生的效果与之前会产生差异
4.应用场景
- JDBC 加载驱动连接 class.forname
- Spring 容器框架 IOC 实例化对象
- 自定义注解生效(反射 + AOP)
- 第三方核心的框架
5.反射技术的使用
使用反射机制初始化对象 获取当前 class 的信息
Class<?> aClass = Class.forName("类的完整路径")//类的完整路径 = 包路径.类名
执行无参构造函数
Class<?> aClass = Class.forName("类的完整路径地址");
UserEntity userEntity = (UserEntity)aClass.newInstance(); //实际上走的是无参构造函数去初始化对象
userEntity.setName("sisyphus");
userEntity.setUserId(1234);
System.out.println((userEntity));
执行有参构造函数
Class<?> aClass = Class.forName("类的完整路径地址");
Constructor<?> constructor = aClass.getConstructor(Integer.class,String.class);
UserEntity userEntity = (UserEntity)constructor.newInstance(10,"sisyphus");
System.out.println(userEntity);
在不指定的情况下,底层默认调用无参,如果没有找到无参构造函数会报错
使用反射机制给属性赋值
Class<?> aClass = Class.forName("类i的完整路径地址");
UserEntity userEntity = (UserEntity)aClass.newInstance();
Field userId = aClass.getDeclaredField("userId");
userId.setAccessible(true);
userId.set(userEntity,12);
Field name = aClass.getDeclaredField("name");
name.setAccessible(true);
name.set(userEntity,"sisyphus");
System.out.println(userEntity);
使用反射机制调用方法
Class<?> aClass = aClass.forName("类的完整路径地址")
UserEntity userEntity = (UserEntity)aClass.newInstance()
Method method = aClass.getDeclareMethod("method",Integer.class)
method.setAccessible(true)
Object invoke = method.invoke(userEntity,10)
sout(invoke)
如果使用反射访问私有属性或者调用私有方法,需要设置权限
使用反射机制获取当前方法上是否有加注解
Transactional delaredAnnotation = method.getDeclaredAnnotation(Transactional.class);
System.out.println(declareAnnotation);
控制台会打印当前方法的 Transactional 事务注解
如果没有,就会打印 null
举例:
@GetMapping("/insertUser")
@Transactional
public String insertUser(UserEntity userEntity){
return userMapper.insertUser(userEntity) > 0 ? "success" : "fail";
}
这个方法上加了 @GetMapping 就可以访问请求,加了 @Transactional 就可以实现事务问题
它们的底层是通过 AOP 和反射技术实现的
二、注解
1.概念
Java 注解是 JDK 5 推出的一个重大特性,可以标记在类、方法、属性上面
内置注解:
@Override - 检查该方法是否为重写方法,如果发现其父类,或者引用接口中并没有该方法时,会报编译错误
@Deprecated - 标记过时方法,如果使用该方法,会报编译警告
@SuppressWarnings - 抑制编译器警告
@Safe Varargs - JDK 7 专门为抑制 ”堆污染“ 警告提供的
@FunctionalInterface - JAVA 8 加入,函数式接口,如果接口中只有一个抽象方法(可以包含多个默认方法或多个 static 方法),该接口称为函数式接口
元注解:
元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。它的作用和目的就是给其他普通的标签进行解释说明的
@Retention - 标识这个注解怎么保存,是只在代码中,还是编入 class 文件,或者是在运行时可以通过反射访问
@Documented - 标记这些注解是否包含在用户文档中
@Target - 标记这个注解应该是哪种 Java 成员
@Inherited - 标记这个注解是继承于哪个注解类(默认情况下注解并没有继承于任何子类)
@Repeatable - 可以在同一个地方使用多次,这是 JAVA 8 加入的特性,在此之前注解在同一个地方只能使用一次(实际上有解决方案,但可读性不好)
注解一共分为三类,除了上面两类还有自定义注解
2.自定义注解的使用
一个简单注解的格式:
@Retention(RetentionPolicy.Runtime) //使得当前注解可以被反射机制获取
@Target({ElementType.METHOD}) //使得当前注解只能用于方法上
public @interface ExtTransactional{
String name();
}
使用该注解的格式:
@ExtTransactional(name = "sisyphus")
public void method(){}
注意:
- 当注解没有定义属性时可以直接使用,如果添加了属性,就必须给属性赋值,给属性赋值的格式为:@Test(String = “sisyphus”)。为了使用注解时方便,还可以给属性指定默认值。这样注解就可以不用赋值直接使用了,格式:String name() default “sisyphus”;
- 我们还可以给自定义注解 Test 添加特殊属性 value(这个名字是固定的)。使用此注解给属性赋值的时候,可以不用写成@Test(value = “sisyphus”),可以简化成 @Test(“sisyphus”)。如果我们给特殊属性 value 设置默认值后,我们就可以直接简化成 @Test
三、Spring事务底层原理
1.为什么要使用事务
我们来看一个例子:
public String insertUser(UserEntity userEntity){
try{
int j = userMapper.insertUser(userEntity);
int i = 1 / 0;
return "ok";
}catch(Exception e){
return "fail";
}
}
在这个 try 代码块中,会先把数据插入数据库,但是我们不知道后面的代码会出错
等到出错的时候,这个时候数据已经被插入数据库了,但这不是我们希望发生的事情
我们加入@Transactional 注解就能解决这个问题
在后面代码出错后,事务会进行回滚,撤销数据插入数据库的操作
但是在某些情况事务可能会失效,就比如我们上面这个例子,如果你真的这么写的话,这个事务是起不到任何作用的
因为如果使用了 try/catch,那么 AOP 外层捕获不到异常,就会认为没有异常,正常执行数据库操作
事务注解底层是基于 AOP 和手动事务去做了封装
2.手动事务
Spring 中事务的类型:
- 手动事务:比如使用 JDBC 的时候,进行的 begin,commit,rollback 等操作
- 声明式事务:基于注解 + AOP,代码简洁,AOP 可以解决代码冗余问题
我们可以去手动回滚数据库操作
public String insertUser(UserEntity userEntity){
try{
int j = userMapper.insertUser(userEntity);
int i = 1 / 0;
return "ok";
}catch(Exception e){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); //加入这个语句就可以手动回滚
return "fail";
}
}
我们把回滚之类的操作封装成一个手动事务
@Component //把手动事务注入到 Spring 容器中
public class TransactionalUtils{
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
//开启事务
public void begin(){
//事务隔离级别属于 mysql
//传播行为属于 Spring,传播行为是指在 Spring 中,a 方法使用到事务,传到 b 方法中也使用到事务
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute());
return transaction;
}
//提交事务
public void commit(){
dataSourceTransactionManager.commit(transaction);
}
//回滚事务
public void rollback(){
dataSourceTransactionManager.rollback(transaction);
}
}
下面是使用该手动事务的方法
private TransactionalUtils transactionalUtils;
public String insertUser(UserEntity userEntity){
TransactionStatus begin = null;
try{
begin = transactionalUtils.begin();
int j = userMapper.insertUser(userEntity);
int i = 1 / 0;
transactionalUtils.commit(begin);
return "ok";
}catch(Exception e){
e.printStackTrace();
if(begin != null)
transactionalUtils.rollback(begin);
return "fail";
}
}
这样的缺点是代码冗余,因此我们需要通过我们的 AOP 技术优化,使其成为声明式事务
3.声明式事务
AOP 技术简单来说就是会在方法前后进行拦截
相关的一些小知识点:
- 切面:拦截器类,其中会定义切点以及通知
- 切点:具体拦截的某个业务点。
- 通知:切面当中的方法,声明通知方法在目标业务层的执行位置,通知类型如下:
- 前置通知:@Before 在目标业务方法执行之前执行
- 后置通知:@After 在目标业务方法执行之后执行
- 返回通知:@AfterReturning 在目标业务方法返回结果之后执行
- 异常通知:@AfterThrowing 在目标业务方法抛出异常之后
- 环绕通知:@Around 功能强大,可代替以上四种通知,还可以控制目标业务方法是否执行以及何时执行
声明式事务使用环绕通知,我们让目标方法在执行之前做一个开启事务,在目标方法执行完毕提交事务或者回滚事务操作
@Aspect
@Component
public class ExtTransactionalAop{
@Autowired
private TransactionalUtils transactionalUtils; //获取手动事务
//如果我们的方法上有我们的自定义注解 ExtTransactional,就执行 Around
@Around(value = "@annotation("注解 ExtTransactional 的完整路径地址")")
public Object around(ProceedingJoinPoint joinPoint){
TransactionStatus begin = null;
try{
begin = transactionalUtils.begin();
Object result = joinPoint.proceed();//表示目标方法,比如我们例子中的 insertUser
transactionalUtils.commit(begin);
return result;
}catch(Throwable throwable){
throwable.printStackTrace();
transactionalUtils.rollback(begin);
return "fail";
}
return null;
}
}
我们可以看到,我们就是在环绕通知中,将手动事务封装起来了
也就是我们之前提到的,事务注解底层是基于 AOP 和手动事务去做了封装
而我们的 insertUser 代码就可以变得异常优雅,如下所示
@ExtTransactional
@GetMapping("/insertUser")
public String insertUser(UserEntity userEntity){
int j = userMaper.insertUser(userEntity)
int i / 0;
return j + "";
}