Spring

为什么要学Spring?

  • 简化开发,降低企业级开发的复杂性
  • 框架整合,高效整合其他技术,提高企业级应用开发与运行效率

Spring Framework系统架构

Spring Framework是Spring生态圈中最基础的项目,是其他项目的根基

分层解耦

为什么要分层解耦?

分层解耦是一种促使系统更易于维护、可测试、可扩展和可重用的设计原则。

  1. 可维护性: 分层设计使得每个层次都相对独立,这使得修改或扩展系统的某一部分变得更容易。当你需要对系统进行更改时,只需关注与当前层次相关的部分,而无需过多涉及其他层次。

  2. 可重用性: 分层设计有助于提高代码的可重用性。每个层次都可以作为一个独立的组件,可以在不同的上下文中重复使用。这种模块化的设计使得将来可以更容易地集成新的功能或将现有的功能用于其他项目。

  3. 可测试性: 分层设计使得系统的不同部分更容易进行单元测试。由于每个层次都有清晰的接口,可以更容易地编写测试用例来验证每个层次的功能。这有助于提高系统的整体质量和稳定性。

  4. 可扩展性: 通过分层设计,可以更容易地添加新的功能或进行系统扩展,而不会对已有的功能产生不必要的影响。新的层次可以被添加到系统中,而不必修改已有的层次。

  5. 降低耦合度: 分层设计有助于降低系统中各个部分之间的耦合度。各层通过定义清晰的接口进行通信,而不直接依赖于其他层的内部实现。这种低耦合度使得系统更加灵活,降低了一个部分变化对其他部分的影响。

三层架构

依赖注入IOC和控制反转DI

IOC控制反转的理解

接口不能直接实例化(不能 new 一个接口),所以传统做法中我们会依赖具体的实现类。而当需要更换实现类时,必须修改所有使用该实现类的地方。

通过引入 IoC(控制反转)和依赖注入(DI),我们可以将实现类的创建和管理交给 Spring 等框架。这样,代码只需要依赖接口,而具体的实现类由框架根据配置自动注入。这种方式不仅减少了代码的耦合度,也使得实现类的更换更加灵活和容易。

DI(Dependency Injection,依赖注入)

DI是 IoC(控制反转)的一种实现方式。它是一种设计模式,用于将对象的依赖关系通过外部配置而不是在代码中硬编码的方式注入到对象中,从而解耦对象之间的依赖。

依赖注入的基本概念

  1. 依赖 :在面向对象编程中,一个类(对象)通常依赖于另一个类(对象)来完成某些功能。例如,Car 类可能依赖于 Engine 类,这意味着 Car 需要一个 Engine 对象来工作。

  2. 注入 :依赖注入的核心思想是由外部实体(通常是 IoC 容器)来负责将依赖对象(如 Engine)传递给依赖者(如 Car),而不是让 Car 自己去创建或查找 Engine

在传统的编程模式中,对象的创建和依赖管理是由对象本身主动处理 的。当引入 IoC 容器后,对象 A 不再主动管理其依赖的对象 B,而是依赖 IoC 容器来创建和注入 这些依赖对象。这样,控制权就从对象 AB 中反转到了 IoC 容器中。

(不使用注解)

IOC,XML
  • 1.管理什么? ( Service与Dao )
  • 2.如何将被管理的对象告知IoC容器 ?(配置)
  • 3 .被管理的对象交给IoC容器,如何获取到IOC容器 ?( 接口 )

<!--bean标签标识配置bean,id属性标示给bean起名宁,class属性表示给bean定义类型-->

    1. IoC容器得到后,如何从容器中获取bean ? ( 接口方法 )
  • 5.使用Spring导入哪些坐标?
DI,XML
  • 1.基于IoC管理bean
    1. Service中使用new形式创建的Dao对象是否保留?(否)
    1. Service中需要的Dao对象如何进入到Service中 ? ( 提供方法 )
  • 4.Service与Dao间的关系如何描述 ? (配置)

<!--property标签表示配置当前bean的属性,name属性表示配置哪一个具体的属性,ref属性表示参照哪一个bean-->

bean的基础配置

在 Spring 中,构成应用程序主干并由 Spring IoC 容器管理的对象称为 bean。bean 是由Spring IoC 容器实例化、组装和管理的对象。

为什么bean默认为单例 ?

管理复用对象,效率更高

适合交给容器进行管理的bean

  • 表现层对象
  • 业务层对象
  • 数据层对象
  • 工具对象

不适合交给容器进行管理的bean

  • 封装实体的域对象

实例化bean的三种方式

通过构造方法实例化Bean
java 复制代码
public class MyBean {
    private String property;

    // 通过构造方法实例化Bean
    public MyBean(String property) {
        this.property = property;
    }

    // Getter and Setter methods
}

// 在应用中
MyBean myBean = new MyBean("someValue");

通过构造方法实例化Bean时,使用的是反射机制调用类的无参数构造方法。反射允许在运行时检查类、获取类的信息以及实例化对象,包括调用构造方法。

静态工厂方法实例化Bean

使用静态方法创建Bean对象,通常在Bean类中提供一个静态方法用于创建实例。

使用Spring等IOC容器的情况下,Bean的实例化工作通常交给容器来完成,开发者只需要在配置文件中声明Bean的定义即可。

实例工厂方法实例化Bean

bean的生命周期

destroy() 方法或 @PreDestroy 注解标记的方法会在Spring容器关闭时(即JVM即将退出时)被调用,从而执行Bean销毁前的操作。

java 复制代码
import org.springframework.beans.factory.DisposableBean;

public class MyBean implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        // 在Bean销毁前执行的操作
        System.out.println("Bean is being destroyed. Cleaning up...");
    }
}

import javax.annotation.PreDestroy;

public class MyBean {
    @PreDestroy
    public void preDestroy() {
        // 在Bean销毁前执行的操作
        System.out.println("Bean is being destroyed. Cleaning up...");
    }
}

依赖注入方式

setter注入

构造器注入

自动注入

集合注入

容器相关

注解开发

定义bean

依赖注入

@autowired它通过 Spring IoC 容器中的 BeanFactory 实现依赖注入。

@Primary用于标识一个Bean作为候选的主要候选项,当多个相同类型的Bean存在时,被标记为@Primary的Bean将被优先选择。

加Primary和删除多余Service注解的区别在于:当代码被作为组件库引入时你是无法修改其源码的,如果此时有需求需要修改注入类就会同时存在多个Service注入。

@Value它可以用于将外部配置文件中的属性值、环境变量、系统属性等注入到 Spring 管理的 Bean 中。

@PropertySource 注解,可以将外部的属性文件(*.properties)加载到 Spring 的环境中,以便在 @Value 注解或 Environment 对象中使用这些属性值。通常用于配置类上,用于指定整个配置类加载的属性文件位置。如果你使用 XML 配置方式,可以使用 <context:property-placeholder> 元素达到类似的效果。

主要方式:

java 复制代码
1.通过类的构造函数将依赖传递给类的实例。
public class Car {
    private Engine engine;
    
    public Car(Engine engine) {
        this.engine = engine;
    }
}
2.通过 setter 方法将依赖传递给类的实例。
public class Car {
    private Engine engine;
    
    public void setEngine(Engine engine) {
        this.engine = engine;
    }
}
3.字段注入:直接通过反射机制将依赖注入到类的字段中,通常使用注解来标记需要注入的字段。
public class Car {
    @Autowired
    private Engine engine;
}
除了常见的 @Autowired 注解,Spring 还提供了其他注解如 @Resource、@Inject 等,这些注解支持依赖注入的不同策略和范围。
4.方法注入:是一种通过调用特定方法传递依赖项的方式。与构造函数注入和Setter方法注入不同,方法注入允许框架在需要时注入依赖项,而不是一次性注入。Spring 中使用 @Lookup 注解来实现方法注入,这允许在运行时为每次调用返回一个新的 Engine 实例。
public class Car {
    private Engine engine;
    
    public void start() {
        this.engine = createEngine();  // createEngine 方法在运行时被注入不同的引擎实例
        engine.run();
    }
    
    @Lookup
    protected Engine createEngine() {
        // 框架将覆盖此方法,返回正确的 Engine 实例
        return null;
    }
}
开发模式
作用范围和生命周期

@Scope :可以定义Bean的实例在Spring容器中的生命周期。

  1. singleton(默认): 在整个Spring容器中只存在一个实例。
  2. prototype: 每次请求时都创建一个新的实例。
  3. request: 在一次HTTP请求中,创建一个新的实例。
  4. session: 在一次用户会话中,创建一个新的实例。
  5. global session: 在一个全局的HTTP Session中,创建一个新的实例(通常用于Portlet应用)。

后三种在Web环境中生效

默认情况下bean的初始化是在容器启动时,使用 @Lazy 注解可以让容器在需要时再进行实例化

**@PostConstruct:**用于标识在对象创建后进行初始化的方法。该方法会在依赖注入完成后,任何其他初始化方法之前被调用。通常用于执行一些初始化操作。

**@PreDestroy:**用于标识在对象销毁前执行的方法。该方法会在容器销毁Bean之前被调用,用于执行一些清理或释放资源的操作。

这两个注解可以与任何方法一起使用,但通常与初始化和销毁方法配合使用,以确保在对象的生命周期中执行必要的操作。在 Java EE 中,这两个注解通常用于 EJB(Enterprise JavaBeans)组件。

获取bean

上述所说的[Spring项目启动时,会把其中的bean都创建好] 还会受到作用域及延迟初始化影响,这里主要针对于默认的单例非延迟加载的bean而言。

第三方bean

如果要管理的bean对象来自于第三方(不是自定义的),是无法用 @Component 及衍生注解声明bean的,就需要用到 @Bean注解,通过@Bean注解的name/value属性指定bean名称,如果未指定,默认是方法名

若要管理的第三方bean对象,建议对这些bean进行集中分类配置,可以通过 @Configuration 注解声明一个配置类。

@Import用于在配置类中导入其他配置类,从而实现配置的模块化和组合。

Spring事务

  • 注解:@Transactional
  • 位置:业务(service)层的方法上、类上、接口上
  • 作用:将当前方法交给sprin进行事务管理,方法执行前,开启事务,成功执行完毕,提交事务,出现异常,回滚事务

默认情况下,只有出现 RuntimeException 才回滚异常。rollbackFor属性用于控制出现何种异常类型,回滚事务。

Spring 中的事务管理机制的底层是由 AOP实现的。

GPT:Spring 的事务管理基于代理模式和 AOP,主要通过以下两种方式来实现:

基于代理的事务管理:

  • Spring 使用代理对象包装目标对象,实现对事务的控制。这里主要使用了 JDK 动态代理或 CGLIB(Code Generation Library)动态代理来创建代理对象。
  • 对于实现接口的目标对象,Spring 使用 JDK 动态代理,而对于没有实现接口的目标对象,Spring 使用 CGLIB 动态代理。
  • 代理对象通过在方法执行前后添加事务管理的逻辑来实现声明式事务。

AOP 事务通知:

  • Spring 的事务管理通过 AOP 的方式,以通知(advice)的形式在目标方法的执行前后添加事务处理逻辑。
  • 在 Spring 中,事务通知主要有 @Transactional 注解和 XML 配置方式两种。@Transactional 注解是基于注解的方式,而 XML 配置方式是基于 XML 文件配置的方式。

Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合

注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务

事务管理器要根据实现技术进行选择,MyBatis框架使用的是JDBC事务

事务角色

  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

rollbackFor

当一个方法被标记为 @Transactional(事务注解),默认情况下只有抛出 RuntimeException 或其子类的异常时才会触发事务回滚。如果你希望在遇到特定类型的异常时也进行回滚,可以使用 rollbackFor 属性来声明。

  • @Transactional(rollbackFor = SomeException.class)
  • @Transactional(rollbackFor = {SomeException.class, AnotherException.class})

如果一个类或者一个类中的 public 方法上被标注@Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。


著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/system-design/framework/spring/spring-transaction.html

传播行为

@Transactional(propagation = propagation.REQUIRED)

NESTED

  • 描述:如果当前存在事务,Spring将为该方法创建一个嵌套事务。嵌套事务允许它回滚,而外部事务可以继续。如果外部事务回滚,嵌套事务也会回滚。
  • 场景:适用于需要部分回滚的场景,比如一组数据库操作中的某个操作失败时希望能够独立回滚,而其他操作继续完成。

事务失效场景及其原因:

1. 方法的可见性问题

  • public方法 : 事务注解@Transactional只能应用在public方法上。如果应用在privateprotectedpackage-private方法上,事务将不会生效。
  • 解决方法 : 确保带有@Transactional注解的方法是public的。

Spring的事务管理是基于AOP(Aspect-Oriented Programming)实现的,具体来说,Spring通过代理模式(Proxy)来管理事务。这种代理机制会在方法调用前后插入额外的逻辑,例如开启和提交事务。

  • 代理模式 : Spring在运行时为带有@Transactional注解的类生成一个代理对象,这个代理对象会拦截对目标方法的调用,然后在调用目标方法之前开启事务,在方法执行后根据执行结果提交或回滚事务。

  • 代理类型:

    • JDK动态代理: 适用于实现了接口的类。代理类实现了相同的接口,并在接口方法被调用时执行事务逻辑。
    • CGLIB代理: 适用于没有实现接口的类。CGLIB通过继承的方式创建子类,并在子类的方法中实现事务逻辑。

@Transactional注解应用在非public方法上时,Spring的AOP机制无法正确地拦截该方法的调用,导致事务管理失效。这主要有以下几个原因:

  • 方法的可见性

    • 如果一个方法是 public,代理对象可以拦截对该方法的调用,进而在调用该方法时应用事务管理。
    • 如果一个方法是 protectedprivate 或包可见性(default),Spring AOP 无法拦截该方法的调用,因此事务管理也不会应用到这些方法上。
  • JDK动态代理的限制:

    • JDK动态代理只能代理接口方法,且这些方法必须是public的。如果在接口方法上添加@Transactional注解,代理机制会正常生效。但是,如果@Transactional注解应用在非public方法上,JDK代理无法生成代理逻辑,因为接口方法默认是public的。
  • CGLIB代理的限制:

    • CGLIB代理通过创建目标类的子类来实现代理。尽管CGLIB可以代理非public方法,但由于Java的访问控制机制,子类无法直接访问父类的private方法。对于protectedpackage-private方法,由于代理对象是通过子类实现的,同样也可能无法正确拦截和处理这些方法的调用。
  • 自调用问题 : 即使你在同一个类中的 public 方法上使用了 @Transactional,如果你在类的内部调用这个方法,事务管理也不会生效。这是因为内部调用不会通过代理对象,而是直接调用,因此事务逻辑不会被触发。

2. 内部方法调用(Self-invocation)

  • 同类中方法相互调用 : 如果在同一个类中,一个带有@Transactional的方法调用另一个带有@Transactional的方法(内部调用),Spring AOP不会代理内部调用,事务可能不会生效。
  • 解决方法 : 将事务逻辑提取到另一个类中,或者在类内部使用 ApplicationContext 获取代理对象自身,然后通过代理对象进行方法调用。
java 复制代码
 @Autowired
    private ApplicationContext context;

    public void someMethod() {
        // 获取代理对象
        MyService proxy = context.getBean(MyService.class);
        proxy.transactionalMethod();
    }

    @Transactional
    public void transactionalMethod() {
        // 事务管理生效
    }

3. 异常处理问题

太难了~面试官让我结合案例讲讲自己对Spring事务传播行为的理解。 (qq.com)

  • 异常被捕获 : 如果事务方法内部抛出的异常被捕获并处理,Spring将不会认为这是一个需要回滚的情况,因此事务不会回滚。Spring的事务管理依赖于未捕获的异常来触发事务的回滚。
  • Spring事务默认情况下会在遇到未经捕获的运行时异常(RuntimeException及其子类)时自动回滚事务。
  • 如果是检查型异常(Checked Exception),则事务不会自动回滚,除非显式配置了回滚。
  • 如果方法A通过try-catch捕获了B方法抛出的异常,Spring将认为这个异常已经得到了处理,不需要回滚事务。因此,事务可能会被提交,而不会回滚。
java 复制代码
@Transactional
public void methodB() {
    // Some database operations
    if (someCondition) {
        throw new RuntimeException("An error occurred in method B");
    }
}
public void methodA() {
    try {
        methodB();
    } catch (Exception e) {
        // Exception is caught and handled
        System.out.println("Exception caught: " + e.getMessage());
    }
    // More code...
}

在这种情况下,methodB抛出的异常被methodA捕获了,因此:

  • 如果methodB的事务是由methodA启动的(即传播行为为Propagation.REQUIRED),捕获了异常后,事务不会回滚。

  • 如果methodBmethodA之外有独立的事务(即传播行为为Propagation.REQUIRES_NEW),那么事务会回滚,但只会影响methodB的事务范围。

  • 不捕获异常: 如果你希望事务在异常时回滚,不要在调用事务方法的地方捕获异常,让它自然传播到事务管理器。

  • 显式回滚 : 在try-catch块内,如果仍然希望事务回滚,可以在catch块中显式地调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();来标记事务为仅回滚。

4. 错误的异常类型

  • 检查型异常(Checked Exception) : 默认情况下,Spring只会对运行时异常(RuntimeException)和错误(Error)进行回滚。对于检查型异常,事务不会自动回滚,除非在@Transactional中显式配置rollbackFor属性。
  • 解决方法 : 在@Transactional注解中明确指定rollbackForrollbackForClassName参数,以对检查型异常进行回滚。

5. 事务传播行为

  • 不当的传播属性 : 使用错误的传播属性可能导致事务失效。例如,Propagation.NOT_SUPPORTED会导致事务被挂起,不在事务上下文中执行。
  • 解决方法 : 根据业务场景选择合适的传播行为,通常使用Propagation.REQUIRED

6. 事务管理器配置错误

  • 多个事务管理器: 如果项目中有多个事务管理器,可能会由于配置不当导致事务失效。
  • 解决方法 : 确保配置了正确的事务管理器,并且在@Transactional中指定了正确的transactionManager

7. 使用了非受控资源

  • 非Spring管理的数据库连接: 如果你在事务内使用了非Spring管理的资源(例如,手动管理的JDBC连接),Spring无法控制这些资源的事务边界,可能导致事务失效。
  • 解决方法: 尽量使用Spring管理的数据源和连接。
JDBC

使用JDBC进行数据库操作的四个主要步骤包括:

1.加载数据库驱动程序:

  • 通过反射加载并注册数据库驱动程序,使得JDBC可以与数据库通信。

2.建立数据库连接:

  • 使用DriverManager类的getConnection()方法与数据库建立连接,通常需要提供数据库的URL、用户名和密码。

3.创建和执行SQL语句:

  • 通过连接对象创建StatementPreparedStatementCallableStatement对象,用于发送SQL语句到数据库执行。
  • Statement用于执行简单的SQL语句,PreparedStatement用于执行带参数的SQL语句,CallableStatement用于调用存储过程。

4.处理结果集和关闭资源:

  • 对于查询操作,使用ResultSet处理返回的数据。
  • 关闭ResultSetStatementConnection对象,以释放资源,避免潜在的内存泄漏。

8. 懒加载异常(LazyInitializationException)

  • 事务结束后访问懒加载数据 : 如果在事务结束后才尝试访问懒加载的实体属性,可能会抛出LazyInitializationException,因为Session已经关闭。
  • 解决方法 : 在事务内完成所有需要的数据加载操作,或者使用@Transactional延长事务的生命周期。

9. 异步方法调用

  • @Async注解 : 异步方法调用默认不在事务上下文中执行,即使该方法有@Transactional注解。
  • 解决方法: 避免在异步方法中进行事务操作,或者确保异步方法本身管理事务。

10.存储引擎不支持事务

  • 底层使用的数据库必须支持事务机制,否则不生效;

Spring的事务隔离级别

  • Spring提供了与数据库相同的隔离级别选项,如DEFAULTREAD_UNCOMMITTEDREAD_COMMITTEDREPEATABLE_READSERIALIZABLE
  • 如果在@Transactional注解中指定了隔离级别,Spring会使用指定的隔离级别来管理事务。

不一致时的行为:

  • 当Spring的隔离级别配置与数据库的默认隔离级别不一致时,Spring的配置会覆盖数据库的默认隔离级别。
  • 这意味着在事务开始时,Spring会通过SQL语句或JDBC API告诉数据库使用Spring配置的隔离级别。

面向切面编程AOP

AOP

是一种编程范式,它主要关注在软件开发中横切关注点的处理。横切关注点指的是那些涉及多个模块或层次的应用的关注点,例如日志、事务、权限控制、异常处理等。AOP 的目标是将这些横切关注点从主要业务逻辑中分离出来,以提高代码的模块化性、可维护性和可读性。

应用场景

  • **日志记录:**记录应用程序的运行时信息,如方法的入参、出参,异常信息等。通过 AOP,可以在各个关键点插入日志记录的逻辑,而不必在每个方法中都编写日志记录代码。

  • **性能监控:**监控方法的执行时间、资源消耗等性能指标。通过 AOP,可以在方法执行前后插入性能监控的逻辑,实现性能数据的收集和分析。

  • 事务管理: 管理数据库事务,确保一组操作要么全部成功,要么全部失败回滚。通过 AOP 的事务通知,可以在方法的开始和结束处添加事务的开始和提交/回滚逻辑,@Transactional注解就是基于 AOP 实现的。

  • 安全性控制: 处理安全性相关的操作,如身份验证、授权等。通过 AOP,可以在方法执行前进行权限检查,确保用户具有执行某个操作的权限。例如,SpringSecurity 利用@PreAuthorize 注解一行代码即可自定义权限校验

  • **缓存管理:**实现数据缓存,避免重复计算或查询。通过 AOP,可以在方法执行前检查缓存中是否存在结果,如果存在则直接返回缓存的数据。Spring Cache 提供了一个集成的、简洁的方式来管理缓存,通过注解和配置自动处理缓存的读取、更新和清除。相比手动实现缓存管理(如使用 AOP),Spring Cache 减少了代码复杂性,并提供了多种缓存管理器的支持,使得缓存管理更加高效和灵活。

  • **异常处理:**统一处理应用程序的异常,记录异常信息,进行统一的异常处理。通过 AOP,可以在方法执行出现异常时捕获异常并进行统一的处理。

优势

  • **模块化:**AOP 提供了一种更好的代码组织方式,允许将关注点(concern)从主要业务逻辑中分离出来。这种分离使得代码更加模块化,易于理解和维护。
  • **可维护性:**AOP 的模块化性质使得系统更容易维护。当关注点需要修改或添加新的关注点时,只需在相应的切面(aspect)中进行修改,而不必修改主要业务逻辑的代码。这有助于降低代码耦合,提高代码的可维护性。
  • **代码重用:**AOP 提供了一种在多个模块中重用关注点的方式。可以将切面定义为可重用的模块,然后在需要的地方引用这些切面。这种重用机制减少了重复编写相同代码的工作,提高了代码的效率。

核心概念

AOP的执行流程

AOP的底层实现主要借助于动态代理技术。

以下是 AOP 的底层实现方式:

  1. JDK 动态代理:

    • JDK 动态代理是通过反射机制在运行时动态创建目标对象的代理对象,实现了对目标对象的包装和控制。
    • 当使用接口定义切面时,Spring 使用 JDK 动态代理。它要求目标对象必须实现一个或多个接口,代理对象将实现这些接口并在代理方法中添加切面逻辑。
  2. CGLIB 动态代理:

    • CGLIB(Code Generation Library)是一个基于字节码操作的代码生成库。它通过修改字节码生成目标对象的子类,从而实现代理。
    • 当目标对象没有实现接口时,Spring 使用 CGLIB 动态代理。CGLIB 可以直接代理类而不需要接口,通过生成目标对象的子类,并在子类中添加切面逻辑来实现代理。

Spring AOP 使用 JDK 动态代理和 CGLIB 动态代理来实现代理对象的创建,具体的选择取决于目标对象是否实现接口。

选择使用JDK动态代理还是CGLIB动态代理的原则是尽量使用JDK动态代理,因为它有以下优势:

  • JDK动态代理是Java标准库的一部分,不需要额外的依赖。
  • JDK动态代理创建的代理对象遵循接口,更符合面向接口编程的原则。
  • JDK动态代理在某些情况下性能更优。

在配置中,可以通过设置proxyTargetClass属性为true来强制使用CGLIB动态代理

XML 复制代码
<aop:config proxy-target-class="true">
    <!-- 配置切面等 -->
</aop:config>

AspectJ 是一种更为全面的 AOP 实现,提供了强大的切面编程支持,通过编译时、类加载时或运行时织入切面逻辑。

实现方式
  • 编译时织入(Compile-time Weaving):使用 AspectJ 编译器(ajc)在编译阶段将切面织入到代码中。
  • 类加载时织入 (Load-time Weaving):通过类加载器在运行时织入切面,通常需要配置 aop.xml 文件和指定 AspectJ 类加载器。
  • 运行时织入(Runtime Weaving):通过 Spring AOP 的代理机制在运行时织入切面(基于 JDK 动态代理或 CGLIB)。
java 复制代码
@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void beforeMethod(JoinPoint joinPoint) {
        System.out.println("Before method: " + joinPoint.getSignature().toShortString());
    }

    @After("execution(* com.example.service.*.*(..))")
    public void afterMethod(JoinPoint joinPoint) {
        System.out.println("After method: " + joinPoint.getSignature().toShortString());
    }
}

通知

执行顺序

切入点表达式

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    String value() default "";
}

public class MyClass {
    @MyAnnotation("This is a custom annotation")
    public void myMethod() {
        // 方法实现
    }
}

在上面的例子中,MyAnnotation 是一个自定义注解,而 @annotation 可能是在某个切面或者其他注解中使用的注解。

java 复制代码
@Aspect
public class MyAspect {
    @Before("@annotation(com.example.MyAnnotation)")
    public void beforeMyAnnotation() {
        // 在带有 @MyAnnotation 注解的方法执行之前执行的逻辑
    }
}

在这个例子中,@Before("@annotation(com.example.MyAnnotation)") 表示在执行带有 @MyAnnotation 注解的方法之前执行 beforeMyAnnotation 方法中的逻辑。

@Retention 注解:

  • 用于指定被注解的注解在源代码、编译时和运行时的保留策略。@Retention 元注解有一个 value 属性,可以取三个枚举值之一:
    • RetentionPolicy.SOURCE: 表示注解仅在源代码中保留,在编译时被丢弃。
    • RetentionPolicy.CLASS: 表示注解在源代码和字节码文件中保留,在运行时被丢弃(默认值)。
    • RetentionPolicy.RUNTIME: 表示注解在源代码、字节码文件和运行时都保留,可以通过反射获取。

@Target 注解:

  • 用于指定被注解的注解可以应用于哪些类型的元素(类、方法、字段等)。@Target 元注解有一个 value 属性,它是一个包含 ElementType 枚举值的数组。
    • ElementType.TYPE: 可以应用于类、接口、枚举。
    • ElementType.METHOD: 可以应用于方法。
    • ElementType.FIELD: 可以应用于字段。
    • 其他 ElementType 枚举值,表示其他类型的元素。

连接点

spring中设计模式

  • 创建型模式:关注对象创建及其实例化过程。
  • 结构型模式:关注类和对象的组合,形成更大的结构。
  • 行为型模式:关注对象间的通信和责任分配。

一、创建型模式(Creational Patterns)

创建型模式主要处理对象的创建,旨在将对象的创建与使用分离,从而提高系统的灵活性和可扩展性。

  1. 单例模式(Singleton Pattern)

    • 确保一个类只有一个实例,并提供全局访问点。
    • 例子:数据库连接池。
  2. 工厂方法模式(Factory Method Pattern)

    • 定义一个用于创建对象的接口,但由子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
    • 例子:日志记录器可以创建不同的日志记录器(文件日志、数据库日志等)。
  3. 抽象工厂模式(Abstract Factory Pattern)

    • 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。
    • 例子:跨平台UI工具包,可以为不同操作系统创建相应的UI组件。
  4. 建造者模式(Builder Pattern)

    • 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
    • 例子:生成不同风格的房子(木屋、别墅等)。
  5. 原型模式(Prototype Pattern)

    • 使用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
    • 例子:克隆一个复杂对象,而不是重新创建它。

1. 单例模式(Singleton Pattern)

应用场景:Spring IoC 容器默认以单例模式管理 bean。

说明:在 Spring 中,默认情况下,Spring 容器为每个定义的 bean 创建一个单例实例。这意味着每次请求该 bean 时,Spring 都会返回同一个实例。

单例 Bean 在 Spring 容器中是线程不安全的。虽然 Spring 容器会为每个单例 Bean 创建一个唯一实例,但这个实例会被多个线程共享,如果这个 Bean 存在可变状态(如成员变量的修改),就可能出现线程安全问题。

Spring 中的设计模式详解 | JavaGuide

线程安全问题的原因

在预加载(也叫饿汉式单例)模式下,单例实例是在类加载时就被创建的。由于类加载时是线程安全的(JVM会保证类加载的线程安全性),所以这种方式不会存在线程安全问题。

java 复制代码
public class Singleton {
    // 类加载时就创建实例
    private static final Singleton instance = new Singleton();

    // 私有化构造函数,防止外部创建实例
    private Singleton() {}

    // 提供静态方法获取单例实例
    public static Singleton getInstance() {
        return instance;
    }
}

首次使用时加载 :对于一个Singleton类,如果它的实现是基于饿汉式(在类加载时就创建实例),那么Singleton类会在第一次被主动使用时加载并初始化。主动使用的情况包括创建类的实例、访问类的静态变量、调用类的静态方法、对类进行反射调用等。类加载包括加载(Loading)、链接(Linking)、初始化(Initialization)三个过程。在初始化阶段,静态变量被初始化,静态块被执行。

懒加载(也叫懒汉式单例)是在第一次调用getInstance()方法时才创建实例。在没有额外的同步措施时,懒加载会存在线程安全问题。

java 复制代码
同步方法
public static synchronized Singleton getInstance() {}

双重检查锁定
private static volatile Singleton instance;

private Singleton() {}
public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

首次调用getInstance()时加载 :在这种情况下,Singleton类仍然是在第一次被主动使用时加载,但实例的创建被延迟到首次调用getInstance()时。

静态内部类的单例模式结合了懒汉式和饿汉式的优点:

java 复制代码
public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        // 静态变量 INSTANCE 在 SingletonHolder 被加载时初始化
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        // 访问 SingletonHolder.INSTANCE 时,SingletonHolder 才会被加载和初始化
        return SingletonHolder.INSTANCE;
    }
}

在静态内部类实现的单例模式中,SingletonHolder类是一个静态内部类,它的特点是:

  • SingletonHolder 不会在外部类 Singleton 被加载时立即初始化 :由于 SingletonHolderSingleton 的静态内部类,JVM 在加载 Singleton 类时,并不会同时加载 SingletonHolder 类。SingletonHolder 类只有在它被真正使用时才会被加载和初始化。

  • 第一次调用 getInstance() 时,SingletonHolder 被初始化 :当第一次调用 getInstance() 方法时,SingletonHolder 类会被加载,JVM 会为它分配内存,并初始化其静态变量 INSTANCE,此时单例实例被创建。

  • 之后的调用直接返回已创建的实例 :由于静态变量 INSTANCE 属于类 SingletonHolder,而类的初始化过程只会执行一次(在类的整个生命周期中),所以之后每次调用 getInstance() 方法时,都可以直接返回已经创建好的 INSTANCE,无需再次初始化。

  • 线程安全:由于类的加载和初始化过程是线程安全的,静态变量的初始化也具有原子性,因此可以确保实例的创建过程是线程安全的。

在Java中,JVM和JIT(即时编译器)也可能对代码进行指令重排序,以提高执行效率。然而,Java的内存模型(Java Memory Model, JMM)规定了一些重排序规则,保证了单线程下的代码执行顺序与代码编写顺序一致(也就是顺序一致性)。

但是,在多线程环境下,重排序可能导致不同线程之间的代码执行顺序不一致,从而引发线程安全问题。

  • instance = new Singleton(); 这行代码实际上可以分为三个步骤:
    1. 分配内存 :为 Singleton 对象分配内存空间。
    2. 调用构造函数 :在分配的内存空间中初始化 Singleton 对象。
    3. 将内存地址赋值给 instance :将 instance 指向刚刚分配的内存地址。

正常情况下,这三个步骤是顺序执行的。但在某些情况下,JVM可能会进行重排序,使得步骤3先于步骤2执行,即:

  1. 分配内存
  2. 将内存地址赋值给 instance (此时 instance 不为空,但对象还未初始化)
  3. 调用构造函数

如果步骤2先执行,那么在构造函数还未执行完之前,另一个线程可能会看到一个非空但未初始化完成的 instance,这就会导致该线程获取到一个不完整的、未正确构造的对象,从而出现线程安全问题。

在Java中,可以通过 volatile 关键字来防止指令重排序。volatile 变量的一个重要特性是,它不仅仅保证变量的可见性,还禁止对其的指令重排序。

  • 可变状态:如果单例 Bean 中有可变的成员变量,这些变量在多个线程之间共享,可能会导致线程安全问题。例如,当多个线程同时访问或修改同一个可变变量时,如果没有正确的同步机制,就可能出现数据不一致或其他并发问题。

  • 无状态Bean 是指它们不保存任何特定的状态数据,不依赖于特定的客户端调用,也不会保存客户端的任何信息。这种Bean可以被多个线程安全地共享和使用,因为它们的行为对于任何调用者都是一致的。

    • 特点

      • 没有实例变量(或实例变量仅用于常量或依赖注入,不改变其值)。
      • 其方法的输出完全依赖于输入参数,不依赖于对象的内部状态。
      • 可以在多个线程之间安全地共享而不需要同步机制。
    • 应用场景

      • 纯粹的工具类Bean,例如格式化工具、转换器、或一些纯粹的业务逻辑服务。
      • 不需要保存特定会话或用户信息的服务,例如静态数据读取服务。
  • 有状态Bean 是指它们维护某种状态信息,这种状态可能会因客户端调用而改变。这意味着Bean在不同的时间或由不同的线程调用时,可能会表现出不同的行为。有状态的单例Bean如果不妥善管理,可能会导致线程安全问题,因为多个线程可能会同时访问和修改Bean的状态。

    • 特点

      • 包含实例变量,这些变量会在运行时发生变化。
      • Bean的行为可能会因其内部状态的改变而不同。
      • 在并发环境下,可能需要通过同步机制来保证线程安全。
    • 应用场景

      • 需要保存用户会话信息、计数器、缓存等状态的服务。
      • 具有配置或状态切换功能的服务。
解决方案
  1. 无状态 Bean:设计成无状态的单例 Bean,避免使用可变的成员变量。无状态的 Bean 不会保存特定用户的会话数据或其他临时状态,只执行一些服务逻辑。这种情况下,线程安全问题不会发生。

  2. 局部变量:将需要在方法内部使用的状态变量作为方法的局部变量,而不是类的成员变量。局部变量是线程私有的,不会有线程安全问题。

  3. 同步机制

    • 加锁 :对可能引起线程安全问题的方法或代码块加锁,如使用 synchronized 关键字。但这可能导致性能下降,因为锁会引起线程的等待和阻塞。
    • 使用并发工具 :使用 java.util.concurrent 包中的并发工具类,如 ReentrantLockAtomicInteger 等,来处理并发问题。
  4. Prototype Scope :对于需要线程隔离的 Bean,可以将其作用域设置为 prototype,每次请求都会创建一个新的实例,从而避免线程安全问题。

  5. ThreadLocal :使用 ThreadLocal 为每个线程提供独立的变量副本,保证每个线程访问的变量都是独立的,不会互相影响。

总结

单例 Bean 本身并不是线程安全的,如果它包含可变状态或在多线程环境中使用时,必须采取适当的措施来确保线程安全。设计无状态的 Bean 或者使用适当的同步机制和并发工具,是保障线程安全的有效方式。

以下是一些不使用单例 Bean 的场景和可能产生的问题:

1. 状态管理问题

场景: 当 Bean 需要持有状态并在每次请求或操作中保持不同的状态时。

问题: 如果一个 Bean 是单例的,并且持有状态,多个线程或请求可能会同时访问和修改这个 Bean 的状态。这可能导致状态不一致、线程安全问题或数据损坏。例如,一个单例 Bean 用于计数器时,如果它被多个线程并发访问,可能需要使用同步机制来保证线程安全。

2. 并发问题

场景: 当 Bean 被多个线程同时使用,且对 Bean 的操作可能影响到其他线程的行为时。

问题: 单例 Bean 在多线程环境下可能会导致并发问题。多个线程同时访问单例 Bean 可能会引发线程安全问题,尤其是在 Bean 的属性被修改时。这要求开发者额外处理线程同步,增加了复杂性。

3. 需要多个不同实例

场景: 当需要创建多个不同的 Bean 实例以满足不同的需求或操作时。

问题 : 单例 Bean 只能创建一个实例。如果业务逻辑要求每次请求都需要一个新的 Bean 实例(例如,为了隔离用户数据),单例 Bean 不适用。此时可以考虑使用 prototype 作用域。

2. 工厂模式(Factory Pattern)

工厂模式(Factory Pattern)是一种创建型设计模式,用于创建对象时不直接暴露创建逻辑,而是通过一个工厂类来负责创建对象。工厂模式的核心思想是将对象的创建与使用分离,使得系统更具灵活性和可扩展性。

应用场景:Spring 使用工厂模式创建 bean 实例。

说明 :Spring 的 BeanFactoryApplicationContext 本身就是工厂,它们根据配置文件或注解创建和管理 bean 实例。

定义接口或抽象类: 工厂模式首先定义一个接口或抽象类,规定对象的创建方法或接口。这个接口或抽象类本身不会实现创建逻辑,而是由具体的子类来实现。

java 复制代码
public interface Product {
    void use();
}

实现工厂类: 工厂类根据不同的条件(例如配置文件、参数)决定创建哪个具体的产品对象。工厂类中包含的创建逻辑是可扩展的。

java 复制代码
public interface ProductFactory {
    Product createProduct();
}

public class ConcreteProductAFactory implements ProductFactory {
    @Override
    public Product createProduct() {
        return new ConcreteProductA();
    }
}

public class ConcreteProductBFactory implements ProductFactory {
    @Override
    public Product createProduct() {
        return new ConcreteProductB();
    }
}

使用工厂创建对象: 客户端代码通过调用工厂类的创建方法来获取所需的产品对象,而不需要直接创建对象。这使得客户端代码与具体产品的实现细节解耦。

java 复制代码
public class Client {
    public static void main(String[] args) {
        ProductFactory factory = new ConcreteProductAFactory();
        Product product = factory.createProduct();
        product.use();
    }
}
  • 在简单工厂模式中,工厂类通常有一个静态方法,通过这个方法根据传入的参数决定创建哪种类型的对象。
  • 工厂方法模式使用继承和多态来处理不同类型对象的创建,每个具体产品都有一个对应的具体工厂。
  • 抽象工厂模式通常用于创建产品族,每个具体工厂创建一组相关的产品。

3.原型模式

原型模式是一种创建型设计模式,它的核心理念是通过**复制(克隆)**现有的对象来创建新对象,而不是通过实例化来创建新的对象。原型模式的主要特点是能够快速创建对象,同时避免了类的重新初始化或创建开销。

在Spring中,scope属性用于定义bean的作用域,其中有一个作用域就叫做prototype,它与单例模式(singleton)形成对比。单例模式是Spring的默认作用域,在这种模式下,Spring容器中只会存在一个bean的实例。而在prototype作用域下,每次从容器中获取bean时,都会创建一个新的实例。

java 复制代码
@Bean
@Scope("prototype")
public MyBean myBean() {
    return new MyBean();
}
  • 配置原型作用域的bean :通过@Scope("prototype")注解或在XML配置中指定scope="prototype",Spring会将该bean定义为原型模式。

  • 获取bean :每次调用ApplicationContext.getBean()方法获取bean时,Spring都会创建并返回一个新的bean实例,而不是返回已有的实例。

  • 原型模式的使用场景

    • 需要频繁创建新对象的场景,例如每个用户请求需要一个新的对象实例。
    • 需要避免状态共享的场景,多个对象实例之间需要完全独立。
    • 某些短生命周期的对象,例如临时数据或辅助类对象。

Java中的深拷贝和浅拷贝确实可以被视为原型模式的例子,特别是在原型模式的实现过程中,拷贝对象的方式是核心部分。在原型模式中,通常通过克隆(即拷贝)现有的对象来创建新对象,而不是通过直接实例化类来创建。这种克隆操作可以是浅拷贝(shallow copy)或深拷贝(deep copy)。

浅拷贝会复制对象的所有字段,但对于引用类型字段(如对象、数组等),它只复制引用地址,而不会复制实际的对象内容。这样,浅拷贝的对象与原对象共享引用类型的数据。

深拷贝不仅复制对象的所有字段,还会递归地复制引用类型字段所指向的对象。这样,深拷贝的对象与原对象完全独立,互不影响。

Cloneable接口 :Java通过Cloneable接口和Object类的clone()方法提供了内置的原型模式支持。实现Cloneable接口并覆盖clone()方法,可以实现对象的浅拷贝或深拷贝。

二.结构型模式

结构型模式关注如何将类和对象组合成更大的结构,以便更好地适应客户端的需求。这类模式主要处理对象之间的关系,帮助系统以各种方式扩展。

常见的结构型模式

  1. 适配器模式(Adapter Pattern):将一个类的接口转换成客户端所期望的另一种接口,使得原本因接口不兼容而无法一起工作的类可以一起工作。

    • 示例:将旧系统的接口适配为新系统所需的接口。
  2. 桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们可以独立变化。

    • 示例:图形绘制系统中的图形和颜色的桥接。
  3. 组合模式(Composite Pattern):将对象组合成树形结构,以表示"部分-整体"的层次结构,使得客户端可以一致地对待单个对象和对象集合。

    • 示例:文件系统中的文件和文件夹的层次结构。
  4. 装饰器模式(Decorator Pattern):动态地给对象添加额外的功能,而不影响其他对象的功能。

    • 示例:为一个图形对象添加滚动条、边框等装饰。
  5. 外观模式(Facade Pattern):为一组接口提供一个统一的高层接口,使得子系统更容易使用。

    • 示例:提供简化的 API 以访问复杂的子系统(如视频播放器、音频播放器)。
  6. 享元模式(Flyweight Pattern):使用共享对象来支持大量的细粒度的对象,提高效率。

    • 示例:文本编辑器中字符的共享表示。
  7. 代理模式(Proxy Pattern):为其他对象提供一种代理以控制对该对象的访问。

    • 示例:虚拟代理(如延迟加载对象)、保护代理(如控制权限)。

4.代理模式(Proxy Pattern)

应用场景:Spring AOP(面向切面编程)使用代理模式。

说明:Spring AOP 使用 JDK 动态代理或 CGLIB 代理在运行时为目标对象创建代理对象,从而在不修改原始代码的情况下添加横切关注点(如事务管理、日志记录等)。

代理模式是一种结构型设计模式,它提供了一个代理对象来控制对目标对象的访问。代理对象可以在目标对象的基础上,添加额外的功能,如权限控制、日志记录、延迟加载等。

Spring中代理模式的应用场景

1. AOP(面向切面编程)

AOP是Spring中使用代理模式的一个典型例子。AOP允许在方法执行的前后,或者在方法抛出异常时,执行一些横切关注点(如日志记录、事务管理、权限控制等)的代码,而不需要修改业务逻辑。

  • JDK动态代理

    Spring AOP默认使用JDK动态代理,如果目标对象实现了接口,Spring会创建一个代理类,这个代理类实现了目标对象的接口,并在接口方法的调用前后添加横切逻辑。JDK动态代理基于Java反射机制。

  • CGLIB代理

    如果目标对象没有实现接口,Spring AOP会使用CGLIB库生成目标对象的子类代理。CGLIB通过字节码生成子类,在子类中覆盖目标方法并添加横切逻辑。

2. 声明式事务管理

Spring的声明式事务管理也是通过代理模式实现的。当在一个方法上声明了事务(例如通过@Transactional注解),Spring会创建该类的代理对象,在代理对象中控制事务的开启、提交或回滚。

  • 事务代理的实现
    当调用被代理的方法时,代理对象会首先检查事务上下文,如果没有现有的事务,则启动一个新事务。在方法执行完成后,代理会根据方法执行结果决定是提交事务还是回滚事务。

代理模式的工作流程

  1. 创建代理对象:Spring根据目标对象的类型选择合适的代理方式(JDK动态代理或CGLIB代理)并创建代理对象。

  2. 方法拦截:当代理对象的方法被调用时,代理对象会拦截该方法调用,执行预先定义的切面逻辑(如事务处理、日志记录)。

  3. 调用目标对象的方法:代理对象在执行完切面逻辑后,会调用目标对象的对应方法。

  4. 执行后置逻辑:目标方法执行完毕后,代理对象可以执行一些后置逻辑,如关闭事务或记录日志。

java 复制代码
1. 定义一个接口
public interface UserService {
    void addUser(String username);
}
2. 实现接口
public class UserServiceImpl implements UserService {

    @Override
    public void addUser(String username) {
        System.out.println("User " + username + " has been added.");
    }
}
3. 创建动态代理处理器
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class UserServiceProxy implements InvocationHandler {

    private final Object target;

    public UserServiceProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在方法执行前加入日志
        System.out.println("Starting method: " + method.getName());

        // 调用目标对象的方法
        Object result = method.invoke(target, args);

        // 在方法执行后加入日志
        System.out.println("Finished method: " + method.getName());

        return result;
    }
}
4. 使用代理
import java.lang.reflect.Proxy;

public class DynamicProxyDemo {

    public static void main(String[] args) {
        // 创建目标对象
        UserService userService = new UserServiceImpl();

        // 创建代理对象
        UserService proxyInstance = (UserService) Proxy.newProxyInstance(
                userService.getClass().getClassLoader(),
                userService.getClass().getInterfaces(),
                new UserServiceProxy(userService)
        );

        // 使用代理对象调用方法
        proxyInstance.addUser("Alice");
    }
}

代理类UserServiceProxy 实现了InvocationHandler接口,通过实现invoke方法,来动态处理代理对象的方法调用。在方法执行前后插入了日志记录逻辑。
Proxy.newProxyInstance 方法创建了一个代理对象,该代理对象在调用addUser方法时,会先执行代理逻辑,然后调用目标对象的addUser方法。

5.享元模式(Flyweight Pattern)

享元模式是一种结构型设计模式,旨在通过共享对象来支持大量细粒度对象的复用,从而减少内存消耗。享元模式的核心思想是将对象的内部状态(可以共享的部分)和外部状态(无法共享的部分)分开,并将共享的部分提取到一个享元对象中。

主要概念

  1. 享元(Flyweight):是可共享的对象,通过共享来减少内存占用。享元对象的状态可以分为内部状态和外部状态:

    • 内部状态:享元对象内部的数据,可以被多个对象共享。
    • 外部状态:在使用享元对象时传递的数据,这部分数据在享元对象外部进行管理。
  2. 享元工厂(Flyweight Factory):负责创建和管理享元对象。它确保享元对象的复用,避免重复创建。

  3. 具体享元(Concrete Flyweight):实现了享元接口的具体类,包含共享的状态。

  4. 非享元(Unshared Flyweight):那些不能被共享的对象。

享元模式的应用场景

  • 内存消耗大的对象:当对象的数量非常庞大且对象之间有很多重复的状态时,享元模式特别有效。
  • 对象共享:当多个对象有相同的状态时,可以通过享元模式共享这些状态。

在Spring中的应用

在Spring框架中,享元模式的应用最明显的例子就是通过Spring的单例模式配置属性的共享来实现的。

1. Bean的单例模式

Spring默认将管理的bean设置为单例模式(除非显式声明为原型模式)。在单例模式下,每个bean在Spring容器中只有一个实例。每次需要这个bean时,Spring都会提供同一个实例,避免了重复创建相同对象的开销,这符合享元模式的思想。

2. 配置属性的共享

Spring的@Value注解和@ConfigurationProperties注解用于注入配置属性。Spring会将这些属性值读取一次,并将其注入到多个需要这些属性的bean中。通过这种方式,Spring有效地共享了配置属性的实例,避免了每个bean重复加载相同的配置数据。

6.适配器模式(Adapter Pattern)

适配器模式是一种结构型设计模式,它用于将一个接口转换成另一个接口,使得原本由于接口不兼容而不能一起工作的类能够一起工作。适配器模式的主要目的是解决接口兼容性问题,提供了一种将类的接口转换成客户端期望的接口的方式。

适配器模式的组成

  1. 目标接口(Target Interface): 客户端期望的接口。
  2. 源接口(Adaptee Interface): 需要适配的接口,通常是不兼容的接口。
  3. 适配器(Adapter): 实现目标接口并将调用委托给源接口的实例。

适配器模式的类型

  1. 对象适配器: 使用组合来实现适配器。适配器持有一个源对象的引用,通过这个引用来调用源对象的方法。
  2. 类适配器: 通过继承来实现适配器。适配器类继承自源类,并实现目标接口。这种方法在 Java 中不常用,因为 Java 不支持多重继承。

在 Spring 框架中,适配器模式(Adapter Pattern)用于解决接口兼容性问题,使得不同接口的类可以协同工作。这个模式常见于 Spring 的 Web 模块中,尤其是在处理控制器和视图的适配时。以下是几个主要的应用场景和实现方式:

1. Spring MVC 中的适配器模式

Spring MVC 使用适配器模式来处理不同类型的控制器。HandlerAdapter 接口就是这个模式的关键。HandlerAdapter 允许 Spring MVC 通过不同的适配器来处理不同类型的处理器(如 @Controller@RestControllerHandlerMethod 等)。

主要组件:

  • HandlerMapping

    • 作用:负责将 HTTP 请求映射到具体的处理器(handler)。它根据请求的 URL 或其他属性决定使用哪个控制器来处理请求。
    • 实现:RequestMappingHandlerMapping 是最常用的实现,根据请求的路径、方法等信息来查找处理器。
  • HandlerAdapter

    • 作用:处理实际的请求。HandlerAdapter 将请求交给对应的处理器(控制器),并将处理结果返回给 DispatcherServlet
    • 实现:RequestMappingHandlerAdapter 是主要的实现,它处理 @RequestMapping 注解的方法。
  • DispatcherServlet

    • 作用:作为前端控制器,接收所有的 HTTP 请求,委派给 HandlerMapping 找到合适的控制器,然后通过 HandlerAdapter 执行请求,最后将结果返回给客户端。
  • ViewResolver

    • 作用:在处理完请求后,将模型数据和视图名称结合,生成最终的视图(如 JSP 页面)。
适配器模式的工作流程

方便理解,偷了张图

【spring】DispatcherServlet详解_spring dispatcherservlet-CSDN博客

  1. 请求接收 : DispatcherServlet 作为前端控制器接收所有 HTTP 请求。

  2. 处理器映射 : DispatcherServlet 通过 HandlerMapping 找到适当的 Handler(控制器或处理器方法)。HandlerMapping 根据请求的 URL 和其他属性返回一个 Handler 对象。

  3. 适配器选择 : DispatcherServlet 通过 HandlerAdapter 适配器将请求交给对应的处理器执行。HandlerAdapter 负责将请求传递给处理器,并处理处理器的返回结果。

  4. 处理请求 : HandlerAdapter 执行具体的处理逻辑,如调用控制器方法,处理请求。

  5. 视图解析 : 处理完成后,HandlerAdapter 返回一个 ModelAndView 对象(包含模型数据和视图名称)。DispatcherServlet 使用 ViewResolver 解析视图名称,并将模型数据传递给视图。

  6. 返回响应: 最终,视图生成响应并返回给客户端。

一个详细的例子来演示 Spring MVC 中四个关键组件(DispatcherServletHandlerMappingHandlerAdapterViewResolver)在一次请求中的具体工作流程。

1.我们定义一个控制器 UserController,它有一个处理请求的方法:

  • @Controller: 标记这是一个控制器。
  • @RequestMapping("/user"): 定义控制器的基础 URL。
  • @GetMapping("/profile") : 处理 /user/profile 请求。
  • ModelAndView : 返回视图名 profileView 和模型数据 username
  1. DispatcherServlet

DispatcherServlet 是 Spring MVC 的前端控制器,它接收所有的 HTTP 请求并进行调度。

  • 请求到达 : 客户端发起请求 GET /user/profile
  • DispatcherServlet 接收请求并负责将请求委派给适当的处理器(控制器)。
  1. HandlerMapping

HandlerMapping 负责将请求映射到适当的处理器。Spring MVC 提供了多种实现,例如 RequestMappingHandlerMapping

  • 查找处理器 : DispatcherServlet 查询 HandlerMapping,寻找处理请求 /user/profile 的处理器。
  • 返回处理器 : RequestMappingHandlerMapping 找到 UserControllergetProfile 方法。
  1. HandlerAdapter

HandlerAdapter 将请求传递给找到的处理器,并处理它的返回值。

  • 选择适配器 : DispatcherServlet 使用 HandlerAdapter 来处理 UserControllergetProfile 方法。
  • 调用处理器 : HandlerAdapter 调用 UserController.getProfile() 方法,执行处理逻辑。
  • 处理结果 : getProfile() 方法返回一个 ModelAndView 对象,其中包含视图名 profileView 和模型数据 username
  1. ViewResolver

ViewResolver 负责将视图名解析成实际的视图实现,并将模型数据传递给视图。

  • 解析视图 : DispatcherServletModelAndView 中的视图名 profileView 传递给 ViewResolver
  • 生成视图 : ViewResolver 将视图名 profileView 解析成实际的视图实现,比如一个 JSP 文件 profileView.jsp
  • 渲染视图 : 将模型数据 username 注入到视图中,并渲染最终的 HTML 响应。

2.Spring Data 中的适配器模式

在 Spring Data 中,适配器模式用于实现不同的数据源和统一的数据访问接口之间的兼容性。Spring Data 的核心目标是简化数据访问层的开发,提供一种通用的方式来操作各种数据存储(如关系数据库、NoSQL 数据库、文档存储等)。适配器模式在 Spring Data 的实现中扮演了关键角色,特别是在 Repository 抽象和数据存储的具体实现之间的适配上。

  1. Repository 接口

在 Spring Data 中,Repository 接口是适配器模式的核心。它定义了数据访问层的通用操作,如 savefindByIddelete 等。Spring Data 提供了多种 Repository 的实现,如 CrudRepositoryJpaRepositoryMongoRepository 等,这些实现适配了不同的数据源。

  1. 适配器模式的实现

Spring Data 使用适配器模式将 Repository 接口与具体的数据存储实现(如 JPA、MongoDB 等)进行连接。具体来说,Spring Data 的适配器模式包含以下几个关键部分:

  • RepositoryFactoryBean : 负责创建 Repository 实现的工厂类。Spring Data 提供了不同的 RepositoryFactoryBean 实现,用于适配不同的数据存储。例如,JpaRepositoryFactoryBean 适配 JPA 数据源,MongoRepositoryFactoryBean 适配 MongoDB 数据源。

  • RepositoryFactory : 创建具体的 Repository 实现。它将 Repository 接口和具体的数据访问操作结合起来。Spring Data 根据 Repository 的类型和配置自动选择合适的 RepositoryFactory 实现。

  • Query Methods: Spring Data 允许通过方法名自动生成查询语句。适配器将这些方法调用转发给底层的数据存储实现。Spring Data 在运行时解析方法名,并生成相应的查询。

  1. 适配器模式的工作流程

  2. 定义接口 : 开发人员定义一个 Repository 接口,声明数据访问操作。

  3. 实现适配器 : Spring Data 提供具体的 RepositoryFactoryBean 实现,用于创建实际的 Repository 实例。

  4. 自动生成实现 : Spring Data 自动生成 Repository 接口的实现,将方法调用适配到底层的数据存储技术(如 JPA、MongoDB)。

  5. 数据访问 : 客户端通过 Repository 接口进行数据访问,Spring Data 处理所有底层的数据操作。

10.装饰者模式

装饰者模式是一种结构型设计模式,允许你动态地将行为附加到对象上。通过使用装饰者模式,可以在不修改对象原有代码的情况下,给对象添加新的功能。装饰者模式的关键在于,装饰者与被装饰的对象必须实现相同的接口或继承相同的基类。

CachingExecutor 与 装饰者模式

MyBatis详解 - 二级缓存实现机制 | Java 全栈知识体系 (pdai.tech)

在 MyBatis 中,CachingExecutor 是对实际执行器 Executor 的一个装饰者,它为 Executor 增加了二级缓存的功能:

  1. 执行流程

    • CachingExecutor 会首先检查二级缓存(通常是应用级别的缓存)中是否存在查询请求的结果。
    • 如果缓存中存在,则直接返回缓存中的结果,避免不必要的数据库查询操作。
    • 如果缓存中不存在,则将查询请求委托给实际的执行器(被装饰的 Executor),由实际的执行器去查询数据库。
    • 查询完成后,CachingExecutor 会将查询结果放入二级缓存中,以便下次请求时能够直接返回缓存的结果。
  2. 为什么是装饰者模式

    • 行为增强CachingExecutor 增强了 Executor 的功能,通过引入缓存机制,减少了数据库查询次数,提高了查询效率。
    • 透明性 :使用 CachingExecutor 不会改变 Executor 的接口,客户端代码仍然调用 Executor 接口的方法,而不需要关心底层是否使用了缓存。
    • 动态组合 :通过装饰者模式,可以将缓存功能按需添加到 Executor 上,而不需要在 Executor 的原始代码中硬编码缓存逻辑,增加了设计的灵活性。

通过这样的设计,CachingExecutor 可以在不修改 BaseExecutor 的前提下,为其添加缓存的功能,这就是典型的装饰者模式的应用。

  • 代理模式

    • 代理对象直接替代原对象,客户端通过代理对象执行所有操作,不直接调用原对象
    • 代理对象主要是为了控制访问,不改变原对象的逻辑。即使代理对象添加了额外的逻辑,最终的业务逻辑由代理本身完成,不一定经过原对象。
  • 装饰者模式

    • 装饰者对象通过组合(持有)原对象来工作。客户端通过装饰者调用方法,但实际操作仍会经过原对象
    • 装饰者通常是通过在方法调用前后增加逻辑,动态地增强原对象的行为。
  • 代理模式:代理类是原对象的替身,控制访问,可能完全取代原对象进行操作。

  • 装饰者模式:装饰者增加功能,但调用最终会经过原对象,保持原有行为并增加新功能。

三. 行为型模式(Behavioral Patterns)

行为型模式关注对象之间的通信,以及如何将任务和责任分配给对象。这类模式主要处理对象的行为和交互,帮助实现更加灵活和动态的系统。

常见的行为型模式

  1. 责任链模式(Chain of Responsibility Pattern):允许将请求沿着处理者链传递,直到某个处理者处理它。

    • 示例:日志处理、请求处理管道。
  2. 命令模式(Command Pattern):将请求封装为对象,从而使你可以参数化客户端代码以执行不同的操作。

    • 示例:菜单操作、撤销操作。
  3. 解释器模式(Interpreter Pattern):为语言的语法定义一个解释器,解释特定的语言。

    • 示例:正则表达式解析、编程语言解析器。
  4. 迭代器模式(Iterator Pattern):提供一种方法顺序访问集合对象中的元素,而不暴露集合的内部表示。

    • 示例:遍历集合的对象(如 List、Set)。
  5. 观察者模式(Observer Pattern):定义一种一对多的依赖关系,使得当一个对象状态变化时,所有依赖于它的对象都会得到通知并自动更新。

    • 示例:事件监听器、发布-订阅系统。
  6. 状态模式(State Pattern):允许对象在内部状态改变时改变其行为,使得对象看起来好像修改了它的类。

    • 示例:状态机、流程控制。
  7. 策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并使它们可以互换,算法的变化不会影响到使用算法的客户端。

    • 示例:支付策略、排序策略。
  8. 模板方法模式(Template Method Pattern):定义一个操作的算法骨架,而将一些步骤延迟到子类中,实现代码复用和扩展。

    • 示例:数据处理流程、算法模板。
  9. 访问者模式(Visitor Pattern):表示一个作用于对象结构中的各个元素的操作,它使你可以在不改变各个元素的类的前提下定义作用于这些元素的新操作。

    • 示例:元素访问与操作(如语法树遍历)。

7.模板方法模式(Template Method Pattern)

模板方法模式是一种行为型设计模式,定义一个操作的算法框架,将一些步骤的实现推迟到子类中。通过这种方式,模板方法模式允许子类在不改变算法结构的情况下,重新定义算法中的某些步骤。

主要概念

  1. 抽象类(Abstract Class):包含模板方法和一些可变步骤的默认实现或抽象方法。模板方法定义了算法的结构,而可变步骤可以由子类提供具体实现。

  2. 模板方法(Template Method):在抽象类中定义的具体方法,按照固定的步骤执行算法。模板方法可以调用一些抽象方法或钩子方法,这些方法在子类中实现或重写。

  3. 具体子类(Concrete Subclass):实现抽象类中的抽象方法或钩子方法,提供特定的实现细节。

主要组成部分

  • 模板方法:定义算法的骨架。
  • 具体步骤:模板方法中定义的各个步骤的具体实现,可以由子类提供。
  • 钩子方法:可选的步骤,子类可以选择是否重写它们。

应用场景

  • 算法步骤固定,但部分步骤可以变化
  • 需要控制算法的执行流程,同时允许某些步骤由子类实现

在Spring中的应用

在Spring框架中,模板方法模式应用得非常广泛,尤其是在Spring的模板类 中。典型的例子包括JdbcTemplateHibernateTemplateTransactionTemplate

1. JdbcTemplate

JdbcTemplate是Spring提供的用于简化JDBC操作的类,它封装了对JDBC的各种操作。JdbcTemplate定义了操作数据库的模板方法,比如查询和更新数据的步骤,同时将具体的SQL查询和结果处理交给子类实现。

2.TransactionTemplate

TransactionTemplate用于简化事务处理,它定义了处理事务的模板方法,允许用户在事务中执行具体的业务逻辑。用户只需提供业务逻辑,而事务的开始、提交和回滚等操作由TransactionTemplate处理。

java 复制代码
public class MyService {
    @Autowired
    private TransactionTemplate transactionTemplate;

    public void performTransactionalOperation() {
        transactionTemplate.execute(status -> {
            // 事务中的业务逻辑
            // 如果发生异常,事务将自动回滚
            return null;
        });
    }
}
3.RestTemplate

RestTemplate 是Spring提供的用于简化与RESTful Web服务交互的同步HTTP客户端。它封装了发送HTTP请求和处理响应的过程,并通过各种方法提供了操作HTTP请求的模板方法。

  1. 模板方法RestTemplate提供了一些高层次的API方法,比如 getForObject(), postForObject(), put(), delete()等,这些方法定义了操作的骨架和顺序。
  2. 具体实现 :底层的HTTP请求和响应处理逻辑被封装在RestTemplate内部,用户只需调用这些高层次的方法即可。具体的HTTP细节(如连接管理、序列化、反序列化等)由RestTemplate处理。
  3. 扩展性 :用户可以通过自定义RequestCallbackResponseExtractor等接口来扩展和定制请求和响应的处理,但整体的操作框架和流程由RestTemplate提供。

8.观察者模式(Observer Pattern)

观察者模式是一种行为型设计模式,用于建立对象之间的一对多依赖关系,使得当一个对象的状态发生变化时,所有依赖于它的对象都会自动收到通知并更新。这个模式常用于事件驱动的系统和发布-订阅系统中。

主要组成部分

  1. 主题(Subject):被观察的对象,维护一系列依赖于它的观察者,并提供方法来添加和移除观察者。
  2. 观察者(Observer):依赖于主题的对象,在主题的状态改变时得到通知并进行更新。
  3. 具体主题(Concrete Subject):实现了主题接口的具体类,持有状态,并在状态改变时通知所有观察者。
  4. 具体观察者(Concrete Observer):实现了观察者接口的具体类,定义了在接到通知时的具体反应。

工作原理

  1. 注册:观察者向主题注册自己以获得通知。
  2. 状态改变:主题的状态发生变化。
  3. 通知:主题通知所有注册的观察者。
  4. 更新:观察者根据接收到的通知更新自己的状态。

1. Spring事件机制

Spring提供了一个事件发布和监听的机制,允许应用程序中的不同组件通过事件来通信。这一机制实现了观察者模式,其中ApplicationEvent作为主题,ApplicationListener作为观察者。

Spring 4.2及以后版本支持使用@EventListener注解来简化事件监听器的实现。通过@EventListener,你可以将事件处理方法直接标记为事件监听器。

Spring的ApplicationContext自身也会发布一些事件,如上下文启动、关闭等。这些事件可以被监听,允许你在特定的应用生命周期阶段插入自定义逻辑。

异步处理
  • 异步事件处理:Spring事件机制支持异步事件处理,允许事件处理逻辑在不同的线程中执行,这对于提升应用的响应性能很有帮助。
java 复制代码
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class AsyncEventListener {

    @Async
    @EventListener
    public void handleCustomEvent(CustomEvent event) {
        System.out.println("Handling event asynchronously: " + event.getMessage());
    }
}

9.责任链模式(Chain of Responsibility Pattern)

责任链模式是一种行为型设计模式,它允许将请求沿着一个链条传递,直到有一个对象处理它为止。该模式可以解耦发送请求的对象和接收请求的对象。它的主要思想是将多个处理者(Handler)组织成一个链条,允许多个处理者处理请求,且请求沿着链条传递,直到链条中的某个处理者处理请求或链条的末端。

主要组成部分

  1. Handler(处理者)

    • 定义一个处理请求的接口,可以选择是否处理请求。如果不能处理,则将请求传递给下一个处理者。
  2. ConcreteHandler(具体处理者)

    • 实现处理请求的接口,决定是否处理请求。如果能处理请求,则处理之;否则,将请求转发给链条中的下一个处理者。
  3. Client(客户端)

    • 向链条中的第一个处理者发送请求,处理者将请求沿着链条传递。

工作原理

  1. 建立链条

    • 处理者对象形成链条,每个处理者都有一个指向下一个处理者的引用。
  2. 发送请求

    • 客户端发送请求到链条中的第一个处理者。
  3. 处理请求

    • 每个处理者检查请求,如果它能够处理请求,则处理它;否则,将请求传递给链条中的下一个处理者。
  4. 请求的终点

    • 如果链条中的所有处理者都不能处理请求,则请求到达链条的末端,可能会得到默认处理或忽略。

在 Spring 框架中,责任链模式的应用主要体现在处理请求和业务逻辑的多个步骤之间的解耦、灵活处理和链式调用。Spring 本身的许多功能和扩展点使用了责任链模式,以下是一些典型的应用场景和示例:

1. Spring Web 中的拦截器

在 Spring MVC 中,拦截器(HandlerInterceptor)可以用于在请求到达控制器之前和响应返回之前进行处理。通过设置多个拦截器,形成一个责任链,对请求进行多阶段处理,例如日志记录、权限检查、性能监控等。

2. Spring AOP(面向切面编程)

Spring AOP 使用代理和切面(Aspect)来处理横切关注点,例如日志记录、安全性检查等。AOP 切面在方法执行之前、之后或者抛出异常时执行额外的操作,这实际上形成了一种责任链模式,其中不同的切面处理不同的横切关注点。

3. Spring Security 中的过滤器链

Spring Security 使用过滤器链来处理安全相关的请求,例如身份验证、授权、CSRF 防护等。每个过滤器负责处理请求链中的特定任务,形成一个责任链。

自定义过滤器

java 复制代码
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.Filter;
import java.io.IOException;

public class CustomFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // Initialization code
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("CustomFilter processing request");
        chain.doFilter(request, response); // Continue with the next filter in the chain
    }

    @Override
    public void destroy() {
        // Cleanup code
    }
}

配置过滤器链

java 复制代码
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SecurityConfig {

    @Bean
    public FilterRegistrationBean<CustomFilter> loggingFilter() {
        FilterRegistrationBean<CustomFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new CustomFilter());
        registrationBean.addUrlPatterns("/*"); // Apply to all URLs
        return registrationBean;
    }
}

4. Spring Boot 中的条件配置

Spring Boot 的 @Conditional 注解和条件配置机制也可以视为一种责任链模式的应用。通过定义条件,可以决定是否启用某些配置或 Bean,这些条件可以是多个的且具有依赖关系。

相关推荐
尘浮生5 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
不是二师兄的八戒28 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
爱编程的小生40 分钟前
Easyexcel(2-文件读取)
java·excel
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
计算机毕设指导62 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
Gu Gu Study2 小时前
枚举与lambda表达式,枚举实现单例模式为什么是安全的,lambda表达式与函数式接口的小九九~
java·开发语言
Chris _data2 小时前
二叉树oj题解析
java·数据结构
牙牙7052 小时前
Centos7安装Jenkins脚本一键部署
java·servlet·jenkins
paopaokaka_luck2 小时前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
以后不吃煲仔饭2 小时前
Java基础夯实——2.7 线程上下文切换
java·开发语言