likes
comments
collection
share

工具使用集| Spring 有感

作者站长头像
站长
· 阅读数 14

再谈 spring 框架

前言

对于spring框架其实都是老生常谈的事情,但是我还想聊聊关于spring中的一些我理解的spring知识。关于spring谈源码,对于刚踏入不久IT行业的我觉得,看源码不能说无用,只是,我觉得收获应该不多。我更倾向,在合适的时间,做合适的事情。

IOC

IOC容器,其实我们可以简单理解为一个Map集合,里面存放:以对象的类型或者名称为 Key、具体的对象为value的一个Map集合。而 Spring为什么能够自动管理这些对象,就是因为使用到了 Java的反射机制。通过反射机制,来动态控制对象的创建与使用,这个也就是我们常说的依赖注入。

Spring通过读取外部的配置文件或者注解(@Autowire@Resource@Inject 等注解)来获取注入对象的信息(例如,它的类型、名称、属性等等)。并且会根据对象是否依赖另一个对象来协调它们的关系。

// spring 会根据反射来帮我们创建对象以及它们的依赖关系
class Persion{
    Address address;
}
管理 Bean 的生命周期
获取 Bean 实例
存储 Bean 实例
创建 Bean 实例
读取配置文件
容器管理Bean的 创建 依赖关系 销毁等工作
确保对象协同工作
应用程序向容器请求对应的 Bean 的名称或 ID
容器返回已创建的 Bean 实例
将对象存放到 Bean 容器中
为每个 Bean 分配唯一的标识符 Bean名称 或 ID
使用 Java 反射机制创建 Bean 实例
调用构造函数或工厂方法创建对象
使用反射技术设置属性值和依赖关系
Spring IoC 容器读取配置文件

我们现在以几个问题来更好的了解Spring IOC:

对于Bean对象直接的依赖关系,IOC容器时如何进行关联的?

它会首先创建所有的Bean对象,然后在创建每个Bean对象的过程中解决它们之间的依赖关系。在解决依赖关系时,Spring容器会查找相应的依赖Bean,如果找到了,则将其注入到当前Bean对象中。如果没有找到,则会抛出异常,表示依赖无法解决。

这里,对象会根据我们编写构造参数或者属性注入,来进行关联他们的依赖。

eg:

public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

此时,IOC容器会先创建UserService对象和UserRepository对象,然后,再容器内部,将UserRepository通过反射技术赋值给UserService对象

IOC容器我们知道了内部实现的原理。对于,我们直接编写的代码,可以很好的理解对象的创建,但是它们是如何读取XML文件来进行对象的创建?

IOC容器一般是通过ClassPathXmlApplicationContextFileSystemXmlApplicationContext来实现。(ClassPathXmlApplicationContext是从类路径(classpath)下读取XML文件,而FileSystemXmlApplicationContext是从指定的文件系统路径下读取XML文件。)

它会读取XML文件的信息,创建BeanDefinition对象(这个对象封装了创建bean所需要的所有信息,比如类名、构造函数、属性值、依赖关系等等。)

BeanDefinition对象是什么

BeanDefinition是一个接口,它定义了描述bean的属性和配置信息的方法。每个被 Spring IoC 容器所管理的对象都对应一个 BeanDefinition 对象,该对象保存了被管理对象的元信息,包括类名、作用域、构造函数、属性值、依赖关系等等。当需要创建对象时,IoC 容器根据相应的 BeanDefinition 来进行对象的创建和属性注入。

创建完BeanDefinition对象之后,容器又会做什么?

IOC容器会将所有解析出来的BeanDefinition对象注册到一个内部的BeanDefinitionRegistry中。(IoC 容器通过将 BeanDefinition 注册到 BeanDefinitionRegistry 中,实现了对 Bean 的管理。在容器启动时,它会遍历所有的 BeanDefinition,根据 BeanDefinition 中的信息创建相应的 Bean,并将这些 Bean 存储到容器中。这样,当应用程序需要使用某个 Bean 时,容器可以快速地从内部数据结构中获取该 Bean 并返回给应用程序。)

BeanDefinitionRegistry是什么,它内部实现是一个Map对象吗?

BeanDefinitionRegistry 是整个 IoC 容器的核心接口,它的作用是管理所有 BeanDefinition 对象的注册、移除和查询。

BeanDefinitionRegistry 中维护了一个 Map,这个 Map 的 key 是每个 Bean 的名称,value 是对应的 BeanDefinition 对象。

BeanDefinitionRegistry 还提供了事件监听机制来监听 BeanDefinition 的变化。

常用的方法包括:

  • registerBeanDefinition(String beanName, BeanDefinition beanDefinition):注册一个 BeanDefinition 对象,指定 Bean 的名称和 BeanDefinition 对象。
  • removeBeanDefinition(String beanName):移除一个已注册的 BeanDefinition 对象,指定 Bean 的名称。
  • getBeanDefinition(String beanName):获取一个已注册的 BeanDefinition 对象,指定 Bean 的名称。
  • containsBeanDefinition(String beanName):判断指定名称的 BeanDefinition 对象是否已经被注册。
  • getBeanDefinitionCount():获取已注册的 BeanDefinition 对象的数量。
  • getBeanDefinitionNames():获取已注册的所有 BeanDefinition 对象的名称。
  • isBeanNameInUse(String beanName):判断指定名称的 Bean 是否已经被注册。
  • void registerBeanDefinition(String beanName, BeanDefinition beanDefinition):在 BeanDefinition 被注册时触发。
  • void removeBeanDefinition(String beanName):在 BeanDefinition 被移除时触发。
  • void removeSingleton(String beanName):在 Bean 对象被移除时触发。

DevTools 模块热部署是不是有使用到BeanDefinition注册监听事件?

热部署可以理解为在应用程序运行期间动态地替换、添加或移除某些组件。在 Spring 中,热部署可以通过刷新应用上下文(refresh)来实现。当应用上下文被刷新时,容器会重新加载所有的 BeanDefinition,从而更新应用程序中的组件。

DevTools 会创建一个 RestartListener 对象,该对象实现了 ApplicationListener 接口,并在应用程序启动时注册到 Spring 的事件机制中。RestartListener 监听应用上下文的启动事件,并在启动时初始化一个用于监测类文件变化的监听器。当类文件发生变化时,监听器会通知 DevTools 触发应用程序重启。

DevTools 模块热部署使用了 BeanDefinition 注册监听事件来实现对类文件变化的监听,并触发应用程序的重启。

说说spring的监听事件?

Spring 容器提供了事件监听机制来监听 BeanDefinition 的变化。这个机制可以用于监听 Bean 的注册、移除等事件,以便在 BeanDefinition 发生变化时及时通知应用程序。这个机制的核心是 ApplicationEventPublisher 和 ApplicationListener 接口。

当容器中的 BeanDefinition 发生变化时,ApplicationEventPublisher 会发布一个事件。ApplicationListener 接口定义了处理事件的方法。应用程序可以通过实现 ApplicationListener 接口来监听 BeanDefinition 的变化,并在事件发生时执行相应的逻辑。在监听器中,我们可以获取到事件源的信息,包括被添加、修改或删除的 BeanDefinition 对象。

监听事件的核心,其实是使用了观察者模式进行实现的。而观察者模式作为核心的点就是:监听事件的监听器维护一系列的观察者。一旦事件发生时,会遍历它所维护的观察者,并调用观察者中的方法,进行事件通知。

//观察者代码案例
// 主题接口
interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}

// 具体主题类
class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
private int state;

public void setState(int state) {
   this.state = state;
   notifyObservers();
}

public int getState() {
   return state;
}

@Override
public void registerObserver(Observer observer) {
   observers.add(observer);
}

@Override
public void removeObserver(Observer observer) {
   observers.remove(observer);
}

@Override
public void notifyObservers() {
   for (Observer observer : observers) {
       observer.update(this);
   }
}
}


// 观察者接口
interface Observer {
void update(Subject subject);
}


// 具体观察者类
class ConcreteObserver implements Observer {
private String name;

public ConcreteObserver(String name) {
   this.name = name;
}

@Override
public void update(Subject subject) {
   int state = ((ConcreteSubject) subject).getState();
   System.out.println("Observer " + name + " received update. New state: " + state);
}
}

// 测试
public class ObserverPatternExample {
public static void main(String[] args) {
   ConcreteSubject subject = new ConcreteSubject();

   Observer observer1 = new ConcreteObserver("Observer 1");
   Observer observer2 = new ConcreteObserver("Observer 2");

   subject.registerObserver(observer1);
   subject.registerObserver(observer2);

   subject.setState(10);
   subject.setState(20);

   subject.removeObserver(observer2);

   subject.setState(30);
}
}

Bean的完整生命周期有那些阶段?

  1. 实例化:读取xml配置文件,调用对象的构造函数创建一个实例。

  2. 赋值操作:创建的对象中的属性并没有具体的值,这一步通过setter方法或直接属性赋值将配置文件中指定的属性值或引用传递给Bean实例。当然这也会将对象类型的值进行赋值操作。

  3. 初始化前回调方法:容器会检查Bean实例是否实现了InitializingBean接口,并在属性赋值后调用其afterPropertiesSet()方法进行一些初始化。

    import org.springframework.beans.factory.InitializingBean;
    
    public class MyBean implements InitializingBean {
    
    @Override
    public void afterPropertiesSet() throws Exception {
       // 在这里编写初始化逻辑
       System.out.println("MyBean初始化完成");
    }
    }
    
    <bean id="myBean" class="com.example.MyBean">
    <!-- 其他属性配置 -->
    </bean>
    
    //相对应,我们可以通过@Autowire注解来指定 bean 名称
    private MyBean myBean;
    
    @Autowired
    public void setMyBean(MyBean myBean) {
    this.myBean = myBean;
    }
    
  4. 自定义初始化方法:容器检查是否有用户自定义的初始化方法。如果有,容器将调用该方法。

    Spring提供了两种方式来指定Bean的初始化方法:

    • 通过在Bean的类定义中使用@PostConstruct注解来标注初始化方法
    public class MyBean {
    
       private String name;
    
       public void setName(String name) {
           this.name = name;
       }
    
       @PostConstruct
       public void init() {
           System.out.println("Bean " + name + " is initializing...");
       }
    }
    
    
     <bean id="userService" class="com.example.UserService" init-method="init">
      <!-- 其他属性配置 -->
     </bean>
     ```
    

    public class UserService {
       // 其他属性和方法
    
       public void init() {
          // 执行初始化逻辑,例如初始化资源、建立数据库连接等
          System.out.println("UserService 初始化方法被调用");
       }
    }
    
    
  5. 初始化后回调方法:容器检查Bean实例是否实现了BeanPostProcessor接口,并在初始化后调用其postProcessBeforeInitialization()方法。

    BeanPostProcessor中的两个核心方法分别是postProcessBeforeInitialization和postProcessAfterInitialization。

    public class MyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
       return bean;
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
       System.out.println("Bean '" + beanName + "' created : " + bean.toString());
       return bean;
    }
    }
    
    
  6. 初始化完成:Bean对象已经准备好。

  7. 销毁前回调方法:容器检查Bean实例是否实现了DisposableBean接口,并在容器关闭时调用其destroy()方法进行清理。

  8. 自定义销毁方法:容器检查是否有用户自定义的销毁方法。如果有,容器将调用该方法。

  9. 完成销毁:Bean实例已被销毁,可以被垃圾收集器回收

其实,大致就分为 创建对象 --- 对象赋值 --- 对象的初始化回调前中后 --- 使用对象 --- 销毁对象 --- 销毁对象的自定义方法 --- 回收对象

Spring还提供了一个Aware接口,它能用来做些什么?Spring管理Bean有帮我们组自己实现这些Aware接口吗

ApplicationContextAware:通过实现该接口,Bean可以获取到ApplicationContext对象,从而可以访问容器中的其他Bean、获取环境配置等。

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class MyBean implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public void doSomething() {
        // 访问容器中的其他Bean
        OtherBean otherBean = applicationContext.getBean(OtherBean.class);
        otherBean.doSomething();

        // 获取环境配置
        String environmentProperty = applicationContext.getEnvironment().getProperty("my.property");
        System.out.println("Environment property value: " + environmentProperty);
    }
}

BeanFactoryAware:通过实现该接口,Bean可以获取到BeanFactory对象,从而可以获取其他Bean的定义信息、创建新的Bean实例等。(使用方式同上)

EnvironmentAware:通过实现该接口,Bean可以获取到Environment对象,从而可以获取运行时环境的属性、配置信息等。(使用方式同上)

总结:

  • 控制反转(IoC)是 Spring 框架的核心思想,而反射则是其实现方式之一,它可以帮助 Spring IoC 容器完成对象的创建和管理
  • 控制反转(IoC)的本质是通过容器来管理对象的生命周期、依赖关系

AOP

切面编程,我们不说那些切面,切点,连接点这些内容,我们谈谈AOP的原理是动态代理

Spring AOP的原理是基于动态代理的实现,主要有两种代理方式:

基于JDK动态代理:如果目标对象实现了接口,Spring AOP就会使用基于JDK的动态代理来创建代理对象。

基于JDK的动态代理,想来应该是老生常谈。我就谈谈自己对Spring AOP 中的基于JDK的动态代理吧。

JDK代理利用了Java的反射机制,在运行时动态地创建一个实现了指定接口的代理类。代理类拦截对目标对象方法的调用,并在方法调用前后执行自定义的逻辑。

当客户端调用代理对象的方法时,代理对象会先执行预定义的逻辑(例如前置处理),然后再调用被代理对象的相应方法,最后执行后置处理逻辑。这样可以在不修改原本被代理对象的代码的情况下,对其方法进行额外的操作和控制。

Proxy类在动态代理中起到了创建代理对象的关键作用,使得我们可以在运行时动态地生成代理对象,并实现对目标对象方法的拦截和处理。(Proxy类可以拦截方法的调用并进行处理的原因在于它生成的代理对象实现了被代理接口)

通过代理对象来调用目标对象的方法,客户端无法直接访问目标对象。代理对象充当了客户端与目标对象之间的中介角色。

jdk动态代理是通过什么方式来拦截的?

JDK动态代理是通过反射实现的。在JDK动态代理中,代理对象实现了InvocationHandler接口,代理方法的具体实现被封装在了InvocationHandler中的invoke方法中。当调用代理对象的方法时,实际上是通过反射机制调用了InvocationHandler中的invoke方法,从而实现了代理。

基于CGLIB代理:如果目标对象没有实现接口,Spring AOP就会使用基于CGLIB的代理来创建代理对象。CGLIB是一个高性能的字节码生成库,可以在运行时动态生成一个子类来实现代理。

CGLIB(Code Generation Library)是一个基于ASM字节码操作库的代码生成库,它可以在运行时动态地生成指定类的子类,实现代理功能。

使用CGLIB实现的动态代理,会创建一个目标对象的子类,子类会拦截目标对象方法的调用并进行增强,因此被代理的对象实际上已经变成了代理对象。

CGLIB 代理的核心类是 Enhancer,它是一个字节码增强器,可以用来扩展一个类并生成其子类。使用 CGLIB 生成的代理类会覆盖被代理类中非 final 的方法,将这些方法拦截并交由用户自定义的拦截器进行处理。

CGLIB 代理的关键方法如下:

  1. Enhancer.create():该方法用于创建代理对象,接收一个被代理类的 Class 对象作为参数,并返回一个代理类的实例。
  2. Enhancer.setCallback():该方法用于设置拦截器,接收一个 MethodInterceptor 接口的实现类作为参数。
  3. MethodInterceptor.intercept():该方法是拦截器的核心方法,当代理对象的方法被调用时会被自动执行,接收三个参数:代理对象、目标方法和参数数组,返回代理方法执行后的结果。

子类会拦截目标对象方法的调用并进行增强是通过什么方式来拦截的?

CGLIB通过继承目标对象生成一个子类,并覆盖目标对象的方法,使得在调用目标对象方法时,实际上是调用了子类的方法,从而达到拦截目标对象方法调用的目的。这个子类中的方法是由CGLIB动态生成的,其实现包括了对切面的调用和目标对象方法的调用。在子类方法中,会先调用切面的逻辑进行增强,然后再调用目标对象的方法。

CGLIB代理的过程大致如下:

1.通过Enhancer创建代理类的对象;

2.为代理类对象设置父类对象,即目标对象;

3.设置拦截器对象,即对应的MethodInterceptor对象;

4.使用代理类对象调用方法时,会被拦截器拦截,从而调用拦截器的intercept方法实现代理功能。

public class UserService {

    public void save() {
        System.out.println("保存用户");
    }

}

public class MyInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("开始事务");
        Object result = methodProxy.invokeSuper(object, args);
        System.out.println("提交事务");
        return result;
    }

}

public class Test {

    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserService.class);
        enhancer.setCallback(new MyInterceptor());
        UserService userService = (UserService) enhancer.create();
        userService.save();
    }

}

字节码生成技术是什么?

字节码生成技术指的是通过编程方式生成字节码,然后在运行时动态加载并执行这些字节码的技术。通常情况下,Java程序是通过编写Java源代码,再将其编译成字节码文件,最终在虚拟机中运行的。但是,有些情况下,我们需要在程序运行时动态地生成字节码,以满足某些特殊需求。字节码生成技术不是直接编写字节码文件,而是使用Java字节码生成框架,比如ASM、CGLIB、Javassist等,在运行时动态生成字节码。

//使用 ASM 动态生成一个类的子类的例子。这个子类重写了 sayHello() 方法,添加了打印日志的逻辑。
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class ASMExample {

    public static void main(String[] args) throws Exception {
        // 定义类的基本信息
        String className = "MyClass";
        String superClassName = "java/lang/Object";

        // 定义方法的基本信息
        String methodName = "sayHello";
        String methodDesc = "()V";

        // 使用 ASM 生成子类的字节码
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, className, null, superClassName, null);

        // 重写 sayHello 方法,添加打印日志的逻辑
        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, methodName, methodDesc, null, null);
        mv.visitCode();
        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("Hello, ASM!");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(2, 1);
        mv.visitEnd();

        // 定义生成的子类的字节数组
        byte[] code = cw.toByteArray();

        // 使用自定义的类加载器加载生成的子类,并创建实例
        ClassLoader classLoader = new ClassLoader() {
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                if (name.equals(className)) {
                    return defineClass(name, code, 0, code.length);
                }
                return super.findClass(name);
            }
        };
        Class<?> clazz = classLoader.loadClass(className);
        Object instance = clazz.newInstance();

        // 调用重写的 sayHello 方法
        clazz.getMethod(methodName).invoke(instance);
    }
}

总结:在Spring AOP中,创建代理对象的方式取决于被代理对象是否实现了接口。如果被代理对象实现了接口,Spring AOP会使用JDK动态代理来创建代理对象;如果被代理对象没有实现接口,Spring AOP会使用CGLIB代理来创建代理对象。

MVC

也是老生常谈了,就谈谈一些重要,比较让人容易忽略的内容吧

Spring MVC框架的核心原理是基于前端控制器(Front Controller)设计模式实现的。前端控制器是一个中央控制器,用于处理Web应用程序中所有请求的入口点,负责协调处理请求的各个组件,以及将响应返回给客户端。Spring MVC框架中的前端控制器称为DispatcherServlet。

DispatcherServlet 是一个 Servlet,它接受来自客户端的所有请求,然后会根据请求进行匹配(简单的字符串匹配、路径匹配等)发送到相应的控制器(Controller)。

  • RequestMappingHandlerMapping:基于注解的 @RequestMapping 映射
  • SimpleUrlHandlerMapping:基于 URL 映射
  • BeanNameUrlHandlerMapping:基于 Bean 名称的映射

DispatcherServlet 初始化过程:

在Servlet容器启动时,DispatcherServlet会被加载,并执行其初始化过程。在初始化过程中,DispatcherServlet会创建一个WebApplicationContext对象,该对象负责管理整个Spring MVC应用程序的Bean实例。WebApplicationContext会根据配置文件中的定义,创建相应的Bean实例,并将它们注册到容器中。

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    // 创建`WebApplicationContext`对象
    this.`WebApplicationContext` = create`WebApplicationContext`();
    // 初始化DispatcherServlet的核心组件
    onRefresh(this.`WebApplicationContext`);
}

DispatcherServlet 处理请求过程:

当DispatcherServlet接收到一个HTTP请求时,它会通过HandlerMapping将请求映射到相应的Controller方法上。Controller方法会根据业务逻辑进行处理,并返回一个ModelAndView对象,该对象包含了处理结果的数据和视图名称。

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 获取HandlerExecutionChain对象
    HandlerExecutionChain chain = getHandler(request);
    if (chain == null) {
        noHandlerFound(request, response);
        return;
    }
    // 获取Controller对象
    Object handler = chain.getHandler();
    // 获取HandlerAdapter对象
    HandlerAdapter handlerAdapter = getHandlerAdapter(handler);
    // 处理请求并返回ModelAndView对象
    ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);
    // 渲染视图
    render(modelAndView, request, response);
}

DispatcherServlet 渲染视图过程:

在处理完请求后,DispatcherServlet需要将处理结果渲染到相应的视图中,并返回给客户端。视图可以是JSP页面、FreeMarker模板、Velocity模板等各种类型。

protected void render(ModelAndView modelAndView, HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 获取ViewResolver对象
    ViewResolver viewResolver = getViewResolver();
    // 根据视图名称获取View对象
    View view = viewResolver.resolveViewName(modelAndView.getViewName(), Locale.getDefault());
    // 渲染视图
    view.render(modelAndView.getModel(), request, response);
}

WebApplicationContext对象是什么?

WebApplicationContext对象是SpringMVC框架中的一个接口,它继承了ApplicationContext接口,用于在Web应用程序中配置和管理Bean。

WebApplicationContext,它是 Spring MVC 的核心之一。在 Servlet 容器启动时,Spring MVC 会创建一个 WebApplicationContext,并加载配置文件中定义的 bean,包括 Controller、ViewResolver、HandlerMapping 等等。DispatcherServlet 通过 WebApplicationContext 来获取这些 bean,完成请求处理流程。

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
         <param-name>contextConfigLocation</param-name>
         <param-value>/WEB-INF/spring/dispatcher-config.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

事务

在Spring中,事务管理可以通过声明式事务和编程式事务两种方式来实现。

声明式事务:通过在配置文件或使用注解的方式声明事务的边界和属性。声明式事务利用了AOP的机制,在方法调用前后应用事务的逻辑,从而实现对事务的管理。在Spring中,常用的声明式事务管理方式是基于@Transactional注解的方式。

编程式事务:通过在代码中显式地编写事务管理逻辑来控制事务的边界和行为。编程式事务需要手动调用事务管理的API来开始、提交或回滚事务,并需要在代码中处理事务的异常情况。虽然较为灵活,但编程式事务通常需要更多的代码编写和管理。

对于以上内容其实也是老生常谈。就不在此扩展,我想说说关于事务的传播行为、隔离级别、只读状态。这些不经常被人简单的知道概念的点。

  1. 传播行为(Propagation):

    • REQUIRED:默认的传播行为,如果当前存在事务,则加入到当前事务中,如果没有事务,则新建一个事务。
    • REQUIRES_NEW:每次都会新建一个事务,如果当前存在事务,则将其挂起。
    • NESTED:如果当前存在事务,则在嵌套事务中执行;如果没有事务,则新建一个事务。嵌套事务是外部事务的一部分,可以回滚或提交。
    • SUPPORTS:支持事务,如果当前存在事务,则加入到当前事务中,如果没有事务,则以非事务方式执行。
    • NOT_SUPPORTED:不支持事务,以非事务方式执行方法,如果当前存在事务,则将其挂起。
    • MANDATORY:强制事务,如果当前存在事务,则加入到当前事务中,如果没有事务,则抛出异常。
    • NEVER:从不使用事务,以非事务方式执行方法,如果当前存在事务,则抛出异常。

如何理解传播行为决定多个事务是否在同一个事务下

在Spring框架中,默认情况下,事务注解仅在声明它们的类内部生效。这意味着如果在Service层的方法上添加了事务注解,那么这些方法将在独立的事务中执行,而不受调用它们的Controller方法的事务控制。

如果希望Controller方法能够控制Service方法的事务行为,需要在Controller方法上添加事务注解。这样,Controller方法将成为一个事务的边界,它的事务行为将影响到调用的Service方法。

当Controller方法上添加了事务注解时,Spring会根据注解的配置来决定事务的传播行为、隔离级别和其他属性。Service方法的事务行为将根据Controller方法的事务属性进行判断,以确定它们是否在同一个事务中。

需要注意的是,Controller方法上的事务属性会覆盖Service方法上的事务属性。如果Controller方法上未添加事务注解,或者事务属性未进行显式配置,则Service方法将在独立的事务中执行。

举个例子:

@Transactional(propagation = Propagation.REQUIRED)
public void myControllerMethod() {
    // 调用Service A方法
    serviceA.methodA();

    // 调用Service B方法
    serviceB.methodB();
}

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    // 业务逻辑
}

@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
    // 业务逻辑
}

按照之前说的,

如果 controller 没有使用注解,方法A和方法B,都将再自己的事务中执行。

如果 controller 使用注解,方法A和方法B,会根据controller层的注解,结合自己方法上的事务注解进行判断,如果controller开启一个事务之后,a 和 b方法都是REQUIRED。那么他们都将在一个事务中进行

再来一个例子:

@Transactional(propagation = Propagation.REQUIRED)
public void myControllerMethod() {
    // 调用Service A方法
    serviceA.methodA();

    // 调用Service B方法
    serviceB.methodB();
}

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    // 业务逻辑
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // 业务逻辑
}

如果 controller 没有使用注解,方法A和方法B,都将再自己的事务中执行。

如果 controller 使用注解,方法A和方法B,会根据controller层的注解,结合自己方法上的事务注解进行判断,如果controller开启一个事务之后:

  1. myControllerMethod方法内部,首先调用serviceA.methodA(),由于methodA也使用了Propagation.REQUIRED传播行为,它会检查当前是否存在事务。由于已经在myControllerMethod方法中创建了事务,因此methodA将加入到当前事务中,共享同一个事务上下文。
  2. 接下来,myControllerMethod方法调用serviceB.methodB(),而methodB标注了Propagation.REQUIRES_NEW传播行为。这意味着无论当前是否存在事务,methodB都将创建一个新的事务,与当前事务相互独立。

举一反三:

@Transactional(propagation = Propagation.MANDATORY)
public void myControllerMethod() {
    // 调用Service A方法
    serviceA.methodA();

    // 调用Service B方法
    serviceB.methodB();
}

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    // 业务逻辑
}


public void methodB() {
    // 业务逻辑
}

思考一下...

如何合理运用事务注解?

假设我们有一个银行系统,用户可以进行转账操作。在转账过程中,我们需要从一个账户扣除一定金额,并将该金额转入另一个账户。同时,我们需要记录转账日志,以便后续进行交易追踪和对账。

在这个例子中,我们可以将扣款和转账放在一个事务中,保证扣款和转账的一致性。而记录转账日志可以放在另一个事务中,保证日志的记录不会影响到扣款和转账的操作。

如果使用Propagation.REQUIRED,即默认的传播行为,当记录转账日志出现异常时,整个事务会回滚,导致扣款和转账操作也会被撤销。但有时我们希望即使记录转账日志失败,扣款和转账操作仍然能够成功,以保证资金的正确流转。以便后续根据转账日志进行追踪处理

这种情况下,我们可以使用Propagation.REQUIRES_NEW,将记录转账日志的操作放在一个独立的事务中

@Service
public class BankService {
    
    @Autowired
    private AccountRepository accountRepository;
    
    @Autowired
    private TransactionLogRepository transactionLogRepository;
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void transferFunds(String fromAccount, String toAccount, BigDecimal amount) {
        try {
            // 扣款
            deductAmount(fromAccount, amount);
            
            // 转账
            depositAmount(toAccount, amount);
        } catch (Exception e) {
            // 异常处理
            // 可以进行回滚、日志记录等操作
        }
        
        // 记录转账日志
        logTransaction(fromAccount, toAccount, amount);
    }
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void deductAmount(String accountNumber, BigDecimal amount) {
        // 扣除账户金额逻辑
        Account account = accountRepository.findByAccountNumber(accountNumber);
        account.setBalance(account.getBalance().subtract(amount));
        accountRepository.save(account);
    }
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void depositAmount(String accountNumber, BigDecimal amount) {
        // 存入账户金额逻辑
        Account account = accountRepository.findByAccountNumber(accountNumber);
        account.setBalance(account.getBalance().add(amount));
        accountRepository.save(account);
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logTransaction(String fromAccount, String toAccount, BigDecimal amount) {
        // 记录转账日志逻辑
        try {
            TransactionLog transactionLog = new TransactionLog(fromAccount, toAccount, amount, new Date());
            transactionLogRepository.save(transactionLog);
        } catch (Exception e) {
            // 异常处理
            // 可以进行回滚、日志记录等操作
        }
    }
}
  1. 隔离级别(Isolation):

    • DEFAULT:使用默认的隔离级别,通常为数据库的默认隔离级别。

    • READ_UNCOMMITTED:允许读取未提交的数据,可能导致脏读、不可重复读和幻读问题。

    • READ_COMMITTED:只能读取已提交的数据,可以避免脏读问题,但仍可能遇到不可重复读和幻读问题。

    • REPEATABLE_READ:保证在同一事务中多次读取数据时,结果始终一致,可以避免脏读和不可重复读问题,但仍可能遇到幻读问题。

    • SERIALIZABLE:最高级别的隔离级别,完全隔离事务,可以避免脏读、不可重复读和幻读问题,但性能较低。

      SERIALIZABLE隔离级别下,数据库会使用锁机制来确保事务的串行执行。当一个事务访问某个数据时,数据库会对该数据加锁,其他事务需要等待该锁释放后才能继续操作。这样可以保证事务之间的互斥性,避免并发执行引发的问题。
      
  2. 只读状态(ReadOnly):

    • true:标记方法或事务为只读,表示只执行读取操作,不进行数据修改。可以提高性能。
    • false:默认值,允许读取和修改数据。

如何理解隔离级别的含义?

要理解它们,我们需要知道

脏读:

脏读是指在一个事务中读取到了另一个未提交事务所修改的数据。

脏读通常发生在以下场景中:

  • 一个事务在修改数据,但是还未提交,此时另一个事务读取了这个未提交的数据。
  • 修改数据的事务最终回滚,但是读取到这个数据的事务已经使用了这个不正确的数据。

脏读通常发生在数据库事务并发执行的情况下。

例如,假设有两个事务T1和T2,T1正在修改某个数据,并且尚未提交。此时,T2读取了被T1修改但尚未提交的数据,这就是脏读。如果T1最终回滚了修改操作,那么T2读取到的数据实际上是无效的或不正确的。

不可重复读:

不可重复读是指在同一个事务中多次读取同一行数据时。

假设有两个事务T1和T2,T1首先读取了某一行数据,然后T2修改了这行数据并提交。接着,T1再次读取同一行数据时,发现数据已经被修改了,与之前的读取结果不一致,这就是不可重复读。

幻读:

它指的是在一个事务内部多次执行相同的查询,但在不同的时间点上返回了不同的结果集

举个例子,假设有两个事务同时执行以下操作:

事务A:

SELECT * FROM products WHERE category = 'Electronics';

事务B:

INSERT INTO products (name, category) VALUES ('iPhone', 'Electronics');

在事务A执行完第一次查询后,事务B插入了一条新的电子产品数据。当事务A执行第二次查询时,会发现有新的数据出现,导致结果集中出现了之前不存在的行,这就是幻读问题。

幻读问题主要出现在修改操作(如INSERT、UPDATE、DELETE)中,而不是简单的SELECT查询。

简单引申:

在MySQL的MVCC机制中,会为每个事务创建一个快照(Snapshot),用于读取数据。当事务开始时,系统会记录一个事务ID,称为ReadView。该ReadView会包含当前事务开始时数据库中的数据版本信息。

在事务执行期间,如果其他事务对数据进行了修改并提交了,这些修改的数据版本将会有一个较新的事务ID。当一个事务读取数据时,它会根据自己的ReadView,只读取事务ID早于或等于自己的数据版本,即它所看到的是自己开始时的快照数据。这样,即使其他事务对数据进行了修改并提交,当前事务也不会看到这些修改,保证了数据的一致性。

需要注意的是,对于未提交的事务(未提交的修改),它们的数据版本在其他事务的ReadView中是不可见的。也就是说,其他事务在读取数据时,不会看到未提交事务的修改。只有在事务提交后,才会将修改的数据版本对其他事务可见。

因此,MySQL的MVCC机制通过为每个事务创建快照,实现了对已提交事务的数据进行读取,同时对未提交事务的修改进行隐藏,确保了数据的一致性和事务的隔离性。