Spring学习总结---适合初学者的学习总结

Spring学习总结

01_IOC_DI

1.1 spring介绍

1.1.1 spring核心理念

这部分初看会看不太懂,这里只是初步介绍,大致了解即可

Spring 围绕两大核心思想设计,贯穿所有功能模块:

  1. 控制反转(IoC,Inversion of Control)
    • 传统开发中,对象的创建、依赖管理由开发者手动控制(如 new UserService());
    • IoC 反转了这一过程:对象的创建、依赖注入由 Spring 容器(IoC 容器)统一管理,开发者只需定义 “需要什么对象”,无需关心 “对象如何创建”。
    • 例:开发者只需在类上标注 @Component,Spring 会自动扫描并创建该类的实例,存入容器;若需要依赖其他对象,用 @Autowired 标注即可,Spring 会自动从容器中 “注入” 依赖。
  2. 面向切面编程(AOP,Aspect-Oriented Programming)
    • 解决 “横切关注点” 问题:如日志记录、事务管理、权限校验等功能,若在每个业务方法中重复编写,会导致代码冗余、维护困难;
    • AOP 允许将这些 “横切逻辑” 抽离为独立的 “切面(Aspect)”,通过配置指定 “在哪些方法执行前后执行切面逻辑”,实现业务代码与非业务代码的解耦
    • 例:用 @Transactional 标注业务方法,Spring 会通过 AOP 自动在方法执行前开启事务、执行后提交 / 回滚事务,开发者无需手动编写事务控制代码。

1.1.2 Spring 的优势

  1. 低耦合:通过 IoC 容器管理对象依赖,避免硬编码创建对象,降低模块间耦合;
  2. 高扩展性:AOP 支持横切逻辑复用,模块新增功能无需修改原有代码;
  3. 简化开发:封装底层 API(如 JDBC、事务),开发者专注业务逻辑,减少冗余代码;
  4. 生态完善:从单体应用(Spring MVC)到微服务(Spring Cloud),覆盖全场景开发需求;
  5. 社区活跃:作为 Java 主流框架,文档丰富、问题解决方案多,版本更新稳定。

1.2 IOC/DI定义理解

  • IOC(控制反转),是一种编程思想
  • 使用IOC之前,用户每次对需求进行变更之后,需要程序员手动去改程序源代码
  • 使用IOC之后,用户每次变更,不需要再改变源代码,程序会自适应用户需求,本质是将对象的创建、依赖管理的主动权,从程序员编写的业务代码中,转移到 Spring 容器(IOC 容器)中
  • IOC 是思想,DI(依赖注入)是实现 IOC 的具体手段——Spring 通过 DI 完成对象依赖的注入,从而实现了 IOC 的核心思想

1.3 IOC思想理解

场景说明

假设我们要开发一个简单的用户服务:UserService 需要依赖 UserDao 来操作数据,需求可能会变更(比如从 MySQLDao 切换到 OracleDao)。

1.3.1 传统开发方式(无 IOC)

  1. 定义 Dao 接口和实现类
// Dao 接口
public interface UserDao {
    void query(); // 查询用户数据
}
// MySQL 实现
public class MySQLDaoImpl implements UserDao {
    @Override
    public void query() {
        System.out.println("用 MySQL 查询用户数据");
    }
}
// Oracle 实现(未来可能需要切换)
public class OracleDaoImpl implements UserDao {
    @Override
    public void query() {
        System.out.println("用 Oracle 查询用户数据");
    }
}
  1. 定义 Service 类(依赖 Dao)
public class UserServiceImpl {
    // 1. 手动创建依赖对象
    private UserDao userDao = new MySQLDao(); // 直接依赖 MySQLDao 实现类
    // 业务方法:调用 Dao 的查询
    public void getUser() {
        userDao.getUser;
    }
}
  1. 测试运行
public class MyTest {
    public void test(){
        UserService userService = new UserServiceImpl(); // 手动创建 Service 对象
        userService.getUser(); // 输出:用 MySQL 查询用户数据
    }
}

问题分析

  • 控制权在代码中UserService 自己创建 MySQLDaonew MySQLDao()),一旦需求变更(要切换到 OracleDao),必须修改 UserService 的源代码(把 new MySQLDao() 改成 new OracleDao())。
  • 耦合度高Service 与具体的 Dao 实现类强绑定,扩展或修改时需要改动多处代码。

1.3.2 Spring IOC 开发方式(控制反转)

  1. 保持 Dao 接口和实现类不变(同上)

  2. 修改 Service 类

public class UserServiceImpl {
    // 1. 只定义依赖接口,不手动创建对象(无 new 关键字)
    private UserDao userDao;
    // 2. 提供 Set 方法
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    // 业务方法:调用 Dao 的查询
    public void getUser() {
        userDao.getUser();
    }
}
  1. 测试
@Test
public void test(){
    UserServiceImpl service = new UserServiceImpl();
    service.setUserDao( new MySqlDaoImpl() );
    service.getUser();
    //那我们现在又想用Oracle去实现呢,只需要调用set方法时传入Oracle
    service.setUserDao( new UserDaoOracleImpl() );
    service.getUser();

1.4 Spring的配置说明与应用

1.4.1 Spirng的配置说明

  1. 别名alias:

用以给bean起别名,可以用多次起多个别名

<alias name="user" alias="user1"/>
<alias name="user" alias="user2"/>
  1. bean:

id:bean的唯一标识符

class:bean对象所对应的全限定名:包名+类型

name:也是别名,可以取多个别名,别名之间用空格,逗号,分号隔开皆可

<bean id="oracleImpl" name="o o1,o2;o3" class="com.pp.dao.OracleDaoImpl"/>
  1. import:

可以将多个配置文件导入合并为一个

1.4.2 Spring的应用---HelloSpring

  1. 引入依赖
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.1.10.RELEASE</version>
</dependency>
  1. 创建实体类
public class Hello {
    private String name;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    public void show(){
        System.out.println("Hello,"+ name );
    }
}
  1. 创建spring配置文件,命名为applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--bean就是java对象 , 由Spring创建和管理
		id为对象名,class为类属性-->
    <bean id="hello" class="com.pp.pojo.Hello">
        <property name="name" value="Spring"/>
    </bean>
</beans>
  1. 通过ioc容器获取对象
  • 创建ioc容器 ApplicationContext context = new classPathXmlApplicationContext("beans.xml");
  • 从ioc容器中获取其已创建的对象 Hello hello = (Hello) context.getBean("hello");
  • 调用hello.show();测试

1.4.3 1.3案例的代码Spring版

  1. 保持 Dao 接口和实现类不变(同上)

  2. 保持UserService接口和实现类不变(同上)

  3. 通过 Spring 配置文件定义对象和依赖关系(从new对象转为从spring容器获取对象)

创建 applicationContext.xml 配置文件,声明所有对象和依赖:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 1. 定义 Dao 对象 -->
    <bean id="mysqlImpl" class="com.pp.dao.MySQLDaoImpl"/>
    <bean id="oracleImpl" class="com.pp.dao.OracleDaoImpl"/>

    <!-- 2. 定义 Service 对象,并注入 Dao 依赖 -->
    <bean id="serviceImpl" class="com.pp.dao.UserServiceImpl">
        
        <!--引用另外一个bean , 不是用value 而是用 ref-->
        <property name="UserDao" ref="oracleImpl"/> <!-- 想切换到 Oracle 只需改这里为 ref="oracleImpl" -->
    </bean>
</beans>
  1. 从 Spring 容器获取对象并使用
public void test(){
    // 1. 创建 Spring IOC 容器(加载配置文件,容器会自动创建所有对象并注入依赖)
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

    // 2. 从容器中获取 UserService 对象(无需手动 new)
    UserServiceImpl serviceImpl = (UserServiceImpl)context.getBean("serviceImpl");

    // 3. 调用业务方法
    serviceImpl.getUser(); // 输出:用 MySQL 查询用户数据(若配置 ref="oracleDao" 则输出 Oracle)
}

1.5 ID(依赖注入)的三种核心方式

1.5.1 DI(依赖注入)说明:

  • 依赖:bean对象的创建依赖于容器
  • 注入:bean对象中的所有属性,由容器来注入

DI 是 IOC 的实现手段,核心是“容器为 Bean 注入所需依赖”,主要分为 3 类:

1.5.2 构造器注入,三种赋值方式:

  1. 下标赋值:按构造方法参数的下标匹配(不推荐,参数顺序变了会出错)
<bean id="user" class="com.pp.pojo.User">
      <!-- 下标 0 对应构造方法第一个参数,1 对应第二个 -->
      <constructor-arg index="0" value="pp"/>
      <constructor-arg index="1" value="18"/>
  </bean>
  1. 通过类型创建:按参数类型匹配(不推荐,若有多个同类型参数会冲突)
<bean id="user" class="com.pp.pojo.User">
    <constructor-arg type="java.lang.String" value="pp"/>
    <constructor-arg type="int" value="18"/>
</bean>
  1. 通过参数名设置(推荐)
<bean id="user" class="com.pp.pojo.User">
    <constructor-arg name="name" value="pp"/>
</bean>	

1.5.3 setter注入(通过set方法注入):

适合 “非必填属性”,需满足两个前提:

  • Bean 类必须提供属性的 Set 方法(如 setName(String name));
  • 无参构造方法(Spring 默认调用无参构造创建 Bean,若自定义了有参构造,需显式定义无参构造)。
<!-- 先定义依赖的 Bean(Address) -->
<bean id="address" class="com.pp.pojo.Address">
    <property name="detail" value="北京市海淀区"/>
</bean>
<bean id="student" class="com.pp-pojo.student">
    
    <!--普通值注入(String,int等),用value-->
	<property name="name" value="pp"/>
	
    <!--Bean注入,用ref关联bean的id-->
	<property name="address" ref="address"/>
	
    <!--数组类型-->
	<property name="books">
		<array>
            <value>红楼梦</value>
            <value>西游记</value>
            <value>水浒转</value>
            <value>三国演义</value>
		</array>
	</property>
	
    <!--map类型(key-value 键值对)-->
	<property name="card">
        <map>
            <entry key="身份证" value="123123"/>
            <entry key="银行卡" value="132132"/>
        </map>
	</property>

	<!--Set类型(无重复元素)-->
	<property name="games">
        <set>
            <value>LoL</value>
            <value>CoC</value>
            <value>BoB</value>
        </set>
	</property>

    <!--null(若不配值,属性默认是null)-->
    <property name="wife">
        <null/>
    </property>

    <!--Properties类型(常用于配置键值对,如数据库连接信息)-->
    <property name="info">
        <props>
            <prop key="username">root</prop>
            <prop key="password">root</prop>
        </props>
    </property>
</bean>

1.5.4 命名空间注入(构造器/setter注入简化版)

通过 p/c 命名空间简化 XML 配置,本质是 “语法糖”,底层仍是 Setter / 构造器注入。

  1. 前提:导入命名空间

需在 Spring 配置文件的 <beans> 标签中添加命名空间声明:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       <!-- 导入 p 命名空间(对应 Setter 注入) -->
       xmlns:p="http://www.springframework.org/schema/p"
       <!-- 导入 c 命名空间(对应构造器注入) -->
       xmlns:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">
  1. p 命名空间注入(简化 Setter 注入)

格式:p:属性名="普通值"p:属性名-ref="引用 Bean 的 id"

<!--p命名空间注入,可以直接注入属性的值:property-->
<bean id="user" class="com.pp.pojo.User" 
      p:name="pp"
      p:age="18"
      p:address-ref="address"/>
  1. c命名空间注入(简化构造器注入)

格式:c:属性名="普通值"c:属性名-ref="引用 Bean 的 id"

<!--c命名空间注入,通过构造器注入:construct-args-->
<bean id="user2" class="com.pp.pojo.User"
      c:name="pp"
      c:age="18"
      c:address-ref="address"/>

补充:

从容器获取 Bean:通过 Bean 的 idclass 类型从容器中获取实例

// 方式 1:通过 id 获取(需强转类型)
Book book1 = (Book) context.getBean("book");
// 方式 2:通过 id + 类型获取(无需强转,推荐)(注意:必须是接口.class,不能是其实现类)
Book book2 = context.getBean("book", Book.class);
// 方式 3:通过类型获取(若该类型只有一个 Bean 实例可用)
Book book3 = context.getBean(Book.class);

02_bean的自动装配

2.1 bean的作用域

image-20250731145340437

2.1.1 singleton(单例模式,spring默认模式):

<bean id="user" class="com.pp.pojo.User" c:age="18" c:name="pp" scope="singleton"/>

2.1.2 prototype(原型模式):

每次从容器get对象时,都会产出一个新的对象

<bean id="accountservice" class="com.something.DefaultAccountservice" scope="prototype"/>

2.1.3 其余的模式只能在web开发中使用,不作介绍

2.2 xml自动装配bean

2.2.1 搭建环境

  1. 新建两个实体类,Cat Dog 都有一个叫的方法
public class Cat {
    public void shout() {
        System.out.println("miao~");
    }
}
public class Dog {
    public void shout() {
        System.out.println("wang~");
    }
}
  1. 新建一个用户类 User
public class User {
    private Cat cat;
    private Dog dog;
    
    // 编写set/get/tostring
}
  1. 编写Spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="dog" class="com.pp.pojo.Dog"/>
    <bean id="cat" class="com.pp.pojo.Cat"/>

    <bean id="user" class="com.pp.pojo.User">
        <property name="cat" ref="cat"/>
        <property name="dog" ref="dog"/>
    </bean>
</beans>
  1. 测试
@Test
    public void testMethodAutowire() {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        User user = (User) context.getBean("user");
        user.getCat().shout();
        user.getDog().shout();
    }

2.2.2 autowire byName(按名称自动装配)

由于在手动配置xml过程中,常常发生字母缺漏和大小写等错误,而无法对其进行检查,使得开发效率降低。

此时通过使用byName

<bean id="user" class="com.pp.pojo.User" autowire="byName"/>

再次测试,结果依旧成功输出,这时如果将catbean id改为catXXX

再次测试, 执行时报空指针java.lang.NullPointerException。因为按byName规则找不对应set方法,真正的setCat就没执行,对象就没有初始化,所以调用时就会报空指针错误。

小结

当一个bean节点带有 autowire byName的属性时。

  1. 将查找其类中所有的set方法名,例如setCat,获得将set去掉并且首字母小写的字符串,即cat。
  2. 去spring容器中寻找是否有此字符串名称id的对象。
  3. 如果有,就取出注入;如果没有,就报空指针异常。

2.2.3 autowire byType (按类型自动装配)

使用autowire byType首先需要保证:同一类型的对象,在spring容器中唯一。如果不唯一,会报不唯一的异常:NoUniqueBeanDefinitionException

测试:

  1. 将user的bean配置修改一下 : autowire="byType"

  2. 测试,正常输出

  3. 在注册一个cat 的bean对象!

<bean id="dog" class="com.pp.pojo.Dog"/>
<bean id="cat" class="com.pp.pojo.Cat"/>
<bean id="cat2" class="com.pp.pojo.Cat"/>

<bean id="user" class="com.pp.pojo.User" autowire="byType"/>
  1. 测试,报错:NoUniqueBeanDefinitionException

  2. 删掉cat2,将cat的bean名称改掉!测试!因为是按类型装配,所以并不会报异常,也不影响最后的结果。甚至将id属性去掉,也不影响结果。

2.3 使用注解进行自动装配

准备工作:利用注解的方式注入属性。

  1. 在spring配置文件中引入context文件头
xmlns:context="http://www.springframework.org/schema/context"

http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
  1. 开启属性注解支持!
<context:annotation-config/>

2.3.1 @Autowired

  • @Autowired是按类型自动转配的,不支持id匹配,也就是说id叫啥都一样。

  • 需要导入 spring-aop的包!

测试:

  1. User类里,cat和dog属性加上@Autowired
@Autowired
private Cat cat;
@Autowired
private Dog dog;
  1. 配置文件
<context:annotation-config/>

<bean id="dog" class="com.pp.pojo.Dog"/>
<bean id="cat" class="com.pp.pojo.Cat"/>
<bean id="user" class="com.pp.pojo.User"/>
  1. 测试,成功输出结果

补充:

@Autowired(required=false) 说明:false,对象可以为nulltrue,对象必须存对象,不能为null

//如果允许对象为null,设置required = false,默认为true
@Autowired(required = false)
private Cat cat;

2.3.2 @Qualifier

  • @Autowired是根据类型自动装配的,加上@Qualifier则可以根据byName的方式自动装配

  • @Qualifier不能单独使用

  1. 配置文件修改内容,保证类型存在对象。且名字不为类的默认名字
<bean id="dog1" class="com.pp.pojo.Dog"/>
<bean id="dog2" class="com.pp.pojo.Dog"/>
<bean id="cat1" class="com.pp.pojo.Cat"/>
<bean id="cat2" class="com.pp.pojo.Cat"/>
  1. 没有加@Qualifier测试,直接报错

  2. 在属性上添加@Qualifier注解

@Autowired
@Qualifier(value = "cat2")
private Cat cat;
@Autowired
@Qualifier(value = "dog1")
private Dog dog;
  1. 测试,成功输出结果

2.3.3 @Resource

  • @Resource如有指定的name属性,先按该属性进行byName方式查找装配;
  • 其次再进行默认的byName方式进行装配;
  • 如果以上都不成功,则按byType的方式自动装配。
  • 都不成功,则报异常

测试一:

  1. User类
@Resource(name = "cat2")
private Cat cat;
@Resource
private Dog dog;
  1. xml文件
<bean id="dog" class="com.pp.pojo.Dog"/>
<bean id="cat1" class="com.pp.pojo.Cat"/>
<bean id="cat2" class="com.pp.pojo.Cat"/>
  1. 测试,成功输出结果

测试二:

  1. User类
@Resource
private Cat cat;
@Resource
private Dog dog;
  1. xml文件
<bean id="dog" class="com.pp.pojo.Dog"/>
<bean id="cat1" class="com.pp.pojo.Cat"/>
  1. 测试,成功输出结果,其先进行byName查找,失败;再进行byType查找,成功。

小结:

@Resource兼具@Autowired@Qualifier的效果

03_使用注解开发

在spring4之后,想要使用注解形式,必须得要引入aop的包(引入spring-webmvc依赖时已导入)

在配置文件当中,还得要引入一个context约束

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

</beans>

3.1 bean的实现

  1. 配置扫描哪些包下的注解
<!--指定注解扫描包-->
<context:component-scan base-package="com.pp.pojo"/>
  1. 在指定包下编写类,增加注解
@Component("user")
// 相当于配置文件中 <bean id="user" class="当前注解的类"/>
public class User {
    public String name = "pp";
}
  1. 测试
@Test
public void test(){
    ApplicationContext applicationContext =
        new ClassPathXmlApplicationContext("beans.xml");
    User user = (User) applicationContext.getBean("user");
    System.out.println(user.name);
}

3.2 属性注入

使用注解注入属性

  1. 可以不用提供set方法,直接在直接名上添加@value("值")
@Component("user")
// 相当于配置文件中 <bean id="user" class="当前注解的类"/>
public class User {
    @Value("pp")
    // 相当于配置文件中 <property name="name" value="pp"/>
    public String name;
}

2、如果提供了set方法,在set方法上添加@value("值");

@Component("user")
public class User {

    public String name;

    @Value("pp")
    public void setName(String name) {
        this.name = name;
    }
}

3.3 一些衍生注解

3.3.1 @Component三个衍生注解

为了更好的进行分层,Spring可以使用其它三个注解,功能一样,目前使用哪一个功能都一样。

  • @Controller:web层
  • @Service:service层
  • @Repository:dao层

3.3.2 自动装配注解

在2.3中有讲解

3.3.3 作用域

@scope

  • singleton:默认的,Spring会采用单例模式创建这个对象。关闭工厂,所有的对象都会销毁。
  • prototype:多例模式。关闭工厂,所有的对象不会销毁。内部的垃圾回收机制会回收
@Controller("user")
@Scope("prototype")
public class User {
    @Value("pp")
    public String name;
}

小结

  1. XML与注解比较

    • XML可以适用任何场景 ,结构清晰,维护方便

    • 注解不是自己提供的类使用不了,开发简单方便

  2. xml与注解整合开发:推荐最佳实践

    • xml管理Bean

    • 注解完成属性注入

    • 使用过程中, 可以不用扫描,扫描是为了类上的注解

  3. <context:annotation-config/> 作用:

    • 进行注解驱动注册,从而使注解生效

    • 用于激活那些已经在spring容器里注册过的bean上面的注解,也就是显示的向Spring注册

    • 如果不扫描包,就需要手动配置bean

    • 如果不加注解驱动,则注入的值为null

3.4 基于Java类进行配置

步骤:

1、编写一个实体类,Dog

@Component  //将这个类标注为Spring的一个组件,放到容器中
public class Dog {
    public String name = "dog";
}

2、新建一个config配置包,编写一个MyConfig配置类

@Configuration  //代表这是一个配置类
@Import(MyConfig2.class)  //导入合并其他配置类,类似于配置文件中的 inculde
public class MyConfig {

    @Bean //通过方法注册一个bean,这里的返回值就Bean的类型,方法名就是bean的id!
    public Dog dog(){
        return new Dog();
    }

}

3、测试

@Test
public void test2(){
    ApplicationContext applicationContext =
            new AnnotationConfigApplicationContext(MyConfig.class);
    Dog dog = (Dog) applicationContext.getBean("dog");
    System.out.println(dog.name);
}

04_静态/动态代理模式

4.1 静态代理模式理解

  1. 意义:

让角色专注于自己的事情,本质上是一种分工合作,降低代码耦合性

  1. 角色分析(以租房为例):
  • 抽象角色:一般会使用接口或者抽象类来解决,例如租房这一行为
  • 真实角色:被代理的角色,如房东
  • 代理角色:代理真实角色,代理真实角色后,我们一般会做一些附属操作(签合同,收钱),如中介
  • 客户:访问代理对象的人,如要租房的人

除了租房,日志记录也是很经典的例子:

  • 日志记录:比如某个接口(抽象角色)的实现类(真实角色)是 “用户登录”,代理角色可以在 “登录” 前后自动打印 “请求参数日志”“响应结果日志”,真实角色无需关注日志逻辑;
  1. 静态代理模式的好处:
  • 可以使真实角色的操作更加纯粹,不用去关注一些公共的业务

  • 公共事务交给代理角色,实现了业务的分工

  • 公共业务发生扩展的时候,方便集中管理

  • 本质是遵循了开闭原则(对扩展开放、对修改关闭)和单一职责原则

  1. 缺点:
  • 一个真实角色就会产生一个代理角色,代码量会翻倍,开发效率会变低

4.2 静态代理代码实现

Rent类,即抽象角色

//抽象角色:租房
public interface Rent {
    public void rent();
}

Host类,即真实角色

//真实角色: 房东,房东要出租房子
public class Host implements Rent{
    public void rent() {
        System.out.println("房屋出租");
    }
}

Proxy类,即代理角色

//代理角色:中介
public class Proxy implements Rent {

    private Host host;
    public Proxy() { }
    public Proxy(Host host) {
        this.host = host;
    }

    //租房
    public void rent(){
        seeHouse();
        host.rent();
        fare();
    }
    //看房
    public void seeHouse(){
        System.out.println("带房客看房");
    }
    //收中介费
    public void fare(){
        System.out.println("收中介费");
    }
}

Client类,即客户

//客户类,一般客户都会去找代理!
public class Client {
    public static void main(String[] args) {
        //房东要租房
        Host host = new Host();
        //中介帮助房东
        Proxy proxy = new Proxy(host);

        //你去找中介
        proxy.rent();
    }
}

4.3 动态代理

Proxy(代理)和 InvocationHandler(调用处理程序)是两个核心组件,它们共同实现了动态代理功能

4.3.1 InvocationHandler(调用处理程序)

InvocationHandler 是一个接口,它定义了代理对象方法被调用时的处理逻辑。当我们通过代理对象调用任何方法时,这个调用都会被转发到 InvocationHandlerinvoke 方法进行处理。

核心方法

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
  • proxy:生成的代理对象本身。
  • method:被调用的目标方法(Method 对象)。
  • args:调用方法时传入的参数数组。
  • 返回值:方法调用的返回结果(需与目标方法的返回类型匹配)。

作用
invoke 方法中,我们可以自定义处理逻辑,例如:

  • 调用目标方法前的预处理(如日志记录)。
  • 调用目标方法(通过 method.invoke(target, args))。
  • 调用目标方法后的后续处理(如结果缓存)。

4.3.2 Proxy(代理类)

Proxy 是 Java 提供的一个工具类,用于在运行时动态生成接口的代理实例。它不能直接实例化,而是通过静态方法 newProxyInstance 创建代理对象。

核心方法

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException;
  • loader:类加载器(通常使用目标对象的类加载器)。
  • interfaces:代理对象需要实现的接口数组(必须是目标对象实现的接口)。
  • h:关联的 InvocationHandler 实例(代理方法调用的处理器)。
  • 返回值:动态生成的代理对象(实现了 interfaces 中的所有接口)。

作用

  • 动态生成代理类的字节码并加载到 JVM 中。
  • 确保代理对象实现指定的接口,从而可以替代目标对象进行调用。
  • 将代理对象的所有方法调用转发给关联的 InvocationHandler 处理。

4.3.3 实例代码

  1. 定义一个接口
interface UserService {
    void login(String username);
}
  1. 接口实现类(目标对象)
class UserServiceImpl implements UserService {
    @Override
    public void login(String username) {
        System.out.println(username + " 登录成功");
    }
}
  1. 自定义 InvocationHandler
class ProxyInvocationHandler implements InvocationHandler {
    private Object target; // 目标对象(被代理的对象)

    public LogInvocationHandler(Object target) {
        this.target = target;
    }
    
     @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 调用前:记录日志
        System.out.println("方法 " + method.getName() + " 开始执行,参数:" + args[0]);
        
        // 调用目标方法
        Object result = method.invoke(target, args);
        
        // 调用后:记录日志
        System.out.println("方法 " + method.getName() + " 执行结束");
        return result;
    }
}
  1. demo
public class ProxyDemo {
    public static void main(String[] args) {
        // 创建目标对象
        UserService target = new UserServiceImpl();
        
        // 创建 InvocationHandler(关联目标对象)
        InvocationHandler pih = new ProxyInvocationHandler(target);
        
        // 生成代理对象(通过 Proxy 动态创建)
        UserService proxy = (UserService) Proxy.newProxyInstance(
            target.getClass().getClassLoader(), // 类加载器
            target.getClass().getInterfaces(),  // 实现的接口
            handler                             // 调用处理器
        );
        
        // 通过代理对象调用方法
        proxy.login("pp");
    }
}

小结:

  • InvocationHandler 负责定义代理逻辑(如何处理方法调用)。
  • Proxy 负责生成代理对象,并将方法调用转发给 InvocationHandler
  • 两者结合实现了动态代理,常用于 AOP(面向切面编程)、日志记录、事务管理等场景,无需手动编写代理类,提高了代码的灵活性和可维护性。
  • 动态代理想要理清楚原理会比较困难,最好阅读多一点文章/视频

05_AOP实现方式

5.1 AOP理解

5.1.1 什么是AOP

image-20250902155103059

在原本一个纵向开发的整个业务中,某一段业务里进行横向拓展开发,就是aop,其作用:

  1. 保持核心业务逻辑(纵向流程)的纯净性
  2. 将那些横向的、跨多个业务的功能抽取出来,形成独立的 "切面"
  3. 通过配置的方式,让这些切面在合适的时机(如方法执行前 / 后)自动介入业务流程

5.1.2 Aop在Spring中的作用

以下名词需要了解下:

  • 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 ....
  • 切面(ASPECT):横切关注点 被模块化 的特殊对象。即,它是一个类。
  • 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。
  • 目标(Target):被通知对象。
  • 代理(Proxy):向目标对象应用通知之后创建的对象。
  • 切入点(PointCut):切面通知 执行的 “地点”的定义。
  • 连接点(JointPoint):与切入点匹配的执行点。

SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice:

  • 前置通知:在方法执行之前触发,实现接口是org.springframework.aop.MethodBeforeAdvice,可用于在方法执行前做一些准备工作,如权限校验等。
  • 后置通知:在方法正常执行完成之后触发,实现接口是org.springframework.aop.AfterReturningAdvice,能用于方法执行后的一些处理,像记录方法执行结果等。
  • 环绕通知:围绕方法的前后进行触发,实现接口是org.aopalliance.intercept.MethodInterceptor,可以在方法执行前后添加自定义逻辑,甚至控制方法是否执行。
  • 异常抛出通知:在方法抛出异常时触发,实现接口是org.springframework.aop.ThrowsAdvice,主要用于处理方法执行过程中出现的异常情况,如异常日志记录等。
  • 引介通知:用于在类中增加新的方法或属性,实现接口是org.springframework.aop.IntroductionInterceptor,能对现有类进行增强,添加新的功能。

5.2 使用Spring实现Aop

使用AOP需要先导入一个依赖包:

<!--https://mvnrepository.com/artifact/org.aspectj/aspectjweaver--> <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId> 
    <version>1.9.4</version>
</dependency>

Spring 中实现 AOP 的三种方式,包括使用原生 Spring API 接口、自定义类和注解方式

5.2.1 使用原生 Spring API 接口

  • 需要实现特定的接口(如 MethodBeforeAdvice、AfterReturningAdvice)
  • 通过 XML 配置将通知与切入点关联
  • 适合需要利用 Spring 提供的标准通知类型的场景

举例:

增加前置日志:

public class Log implements MethodBeforeAdvice{
	public void before(Method method, Object[] objects, Object target) throws Throwable {}
}

增加后置日志:

public class AfterLog implements AfterReturningAdvice{
    //returnValue;返回值
	public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {}
}

之后把这些类注册到Spring中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!--注册bean-->
        <bean id="userService" class="com.pp.service.UserServiceImpl"/>
        <bean id="log" class="com.pp.log.Log"/>
        <bean id="afterLog" class="com.pp.log.AfterLog"/>

        <!--方式一:使用原生SpringAPI接口-->
        <!--配置aop:需要导入aop的约束--> 
        <aop:config>
            <!--切入点:expression:表达式,execution(要执行的位置!* * * * *)-->
            <aop:pointcut id="pointcut" expression="execution(* com.pp.service.UserServiceImpl.*(..))"/>

            <!--热行环绕增加!-->
            <aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
            <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
        </aop:config>

5.2.2 自定义类实现 AOP

  • 不需要实现特定接口,更加灵活
  • 在 XML 配置中定义切面、切入点和通知
  • 适合简单的 AOP 需求,代码侵入性低

举例:

写一个自定义的切入类

public class DiyPointcut {

    public void before(){
        System.out.println("方法前");
    }
    public void after(){
        System.out.println("方法后");
    }
    
}

配置xml

<bean id="diy" class="com.pp.diy.DiyPointcut"/>
<aop:config>
    <!--自定义切面,ref要引用的类-->
	<aop:aspect ref="diy">
        <!--切入点-->
        <aop:pointcut id="point" expression="execution(*com.pp.service.UserServiceImpl.*(..))"/>
        <!--通知-->
        <aop:before method="before" pointcut-ref="point"/>
        <aop:after method="after" pointcut-ref="point"/>
	</aop:aspect>
</aop:config>

5.2.3 注解方式实现 AOP

  • 使用 @Aspect、@Before、@After、@Around 等注解
  • 需要在配置文件中添加aop:aspectj-autoproxy/开启支持
  • 代码简洁,配置量少,是目前最常用的方式

步骤

在自定义的类前加 @Aspect

在方法前加如@Before("execution(*com.pp.service.UserServiceImpl.*(..))")

自定义类:

@Aspect
public class AnnotationPointcut {
    @Before("execution(* com.pp.service.UserServiceImpl.*(..))")
    public void before(){
        System.out.println("方法执行前");
    }

    @After("execution(* com.pp.service.UserServiceImpl.*(..))")
    public void after(){
        System.out.println("方法执行后");
    }

    @Around("execution(* com.pp.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("环绕前");
        System.out.println("签名:"+ jp.getSignature());
        //执行目标方法proceed
         Object proceed = jp.proceed();
         System.out.println("环绕后");
         System.out.println(proceed);
    }
}

之后在配置文件中写入bean,以让Test类扫描:

<!--开启注解支持!-->
<aop:aspectj-autoproxy/>
    
<bean id="annotationPointCut" class="com.pp.diy.AnnotationPointCut"/>

执行顺序:
环绕前 -> before -> 执行 -> 环绕后 -> after

06_整合Mybatis

6.1 了解MyBatis-Spring

MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中

依赖:

<!--Spring-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.10.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.1.10.RELEASE</version>
        </dependency>
        <!--junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!--mysql-connector-java-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
        <!--aspectJ AOP 织入器-->
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
        <!--mybatis-spring整合包-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.2</version>
        </dependency>
    </dependencies>

    <!--配置Maven静态资源过滤问题-->
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

要和 Spring 一起使用 MyBatis,需要在 Spring 应用上下文中定义至少两样东西:一个 SqlSessionFactory 和至少一个数据映射器类

6.2 整合实现一

在 MyBatis-Spring 中

  1. dataSource使用Spring创建,替换mybaits的dataSource
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis01?useSSL=true&useUnicode=true&characterEncoding=utf8"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
  1. 使用SqlSessionFactoryBean来创建 SqlSessionFactory,关联MyBatis
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!--关联Mybatis-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <property name="mapperLocations" value="classpath:com/pp/dao/*.xml"/>
    </bean>
  1. 注册sqlSessionTemplate,关联sqlSessionFactory
<!-- sqlSessionTemplate(等同于mybatis的sqlSession) -->
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
        <!-- 仅能使用构造器注入,因为其没有set方法 -->
        <constructor-arg index="0" ref="sqlSessionFactory"/>
    </bean>
  1. 增加Dao接口的实现类;私有化sqlSessionTemplate
public class UserDaoImpl implements UserMapper {

    private SqlSessionTemplate sqlSession;

    public void setSqlSession(SqlSessionTemplate sqlSession) {
        this.sqlSession = sqlSession;
    }

    public List<User> selectUser() {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        return mapper.selectUser();
    }
    
}
  1. 注册bean实现
<bean id="userDao" class="com.pp.dao.UserDaoImpl">
    <property name="sqlSession" ref="sqlSession"/>
</bean>
  1. 测试
@Test
    public void test2(){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserMapper mapper = (UserMapper) context.getBean("userDao");
        List<User> user = mapper.selectUser();
        System.out.println(user);
    }

6.3 整合实现二

dao继承SqlSessionDaoSupport类 , 直接利用 getSqlSession() 获得SqlSessionTemplate , 然后直接注入SqlSessionFactory . 比起实现一 , 不需要管理SqlSessionTemplate , 而且对事务的支持更加友好

相较于实现一,其实就是删去第3步,修改第4步的UserDaoImpl类(继承了SqlSessionDaoSupport,可以直接利用 getSqlSession()获取SqlSessionTemplate ):

public class UserDaoImpl extends SqlSessionDaoSupport implements UserMapper {
    public List<User> selectUser() {
        return getSqlSession().getMapper(UserMapper.class)r.selectUser();
    }
}

修改第五步bean的配置:

<bean id="userDao" class="com.kuang.dao.UserDaoImpl">
    <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
  1. 测试

07_声明式事务

7.1 事务回顾

7.1.1 事务的特点

  • 把一组业务当成一个总的业务来做,要么都成功要么都失败
  • 事务在项目开发过程非常重要,涉及到数据的一致性的问题,用来确保数据的完整性和一致性

7.1.2 事务ACID原则

  • 原子性
  • 一致性
  • 隔离性:每个事务都应该与其他事务隔离开来,防止数据损坏
  • 持久性:事务一旦完成,事务的结果被写到持久化存储器中

7.1.3 例子理解

比如一个方法里调用了两个方法,add和delete,add成功了,delete出问题没成功,但add却成功修改了数据,这种只完成了一半是不符合原则的

7.2 Spring中的事务管理

  • 编程式事务管理:将事务管理代码嵌到业务方法中来控制事务的提交和回滚
  • 声明式事务管理:将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理
  1. 使用Spring管理事务,注意头文件的约束导入 : tx
xmlns:tx="http://www.springframework.org/schema/tx"

http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

2. 配置事务管理器

  • 无论使用Spring的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的,它负责与底层数据库交互,实现事务的提交、回滚等操作。
  • 就是 Spring的核心事务管理抽象,管理封装了一组独立于技术的方法。
  • Spring 为不同的数据访问技术提供了对应的事务管理器:
    • DataSourceTransactionManager:用于 JDBC 或 MyBatis(基于 JDBC)
    • HibernateTransactionManager:用于 Hibernate
    • JpaTransactionManager:用于 JPA

如JDBC事务

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
 </bean>
  1. 配置事务的通知

事务通知(Transaction Advice)用于定义哪些方法需要事务支持以及事务的具体规则(如传播特性、隔离级别、超时时间等)。

<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!--配置哪些方法使用什么样的事务,配置事务的传播特性-->
        <tx:method name="add" propagation="REQUIRED"/>
        <tx:method name="delete" propagation="REQUIRED"/>
        <tx:method name="update" propagation="REQUIRED"/>
        <tx:method name="search*" propagation="REQUIRED"/>
        <tx:method name="get" read-only="true"/>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

关键属性说明:

  • id="txAdvice":事务通知的唯一标识,用于后续 AOP 织入。
  • transaction-manager="transactionManager":关联前面定义的事务管理器。
  • <tx:attributes>:包含多个 <tx:method>,每个 <tx:method> 对应一组方法的事务规则。

<tx:method> 常用属性:

  • name:指定方法名,可以使用通配符(* 表示任意字符)。例如:
    • name="add":匹配所有名为 add 的方法。
    • name="search*":匹配所有以 search 开头的方法(如 searchByIdsearchAll)。
    • name="*":匹配所有方法(通常作为默认规则放在最后)。
  • propagation:事务传播特性(核心),定义了多个事务方法相互调用时,事务如何传递。常用值:
    • REQUIRED(默认):如果当前没有事务,就创建一个新事务;如果当前已有事务,就加入该事务(最常用)。
    • REQUIRES_NEW:无论当前是否有事务,都创建一个新事务(新事务独立于原事务)。
    • SUPPORTS:如果当前有事务,就加入;如果没有,就以非事务方式执行。
    • MANDATORY:必须在事务中执行,否则抛出异常。
  • read-only:是否为只读事务。read-only="true" 表示该方法只查询数据,不修改数据,Spring 会进行优化(如关闭事务自动提交)。通常查询方法设置为 true,增删改方法设置为 false(默认)。
  • isolation:事务隔离级别,解决并发问题(如脏读、不可重复读、幻读)。默认使用数据库的隔离级别,常用值:
    • DEFAULT(默认):使用数据库的默认隔离级别。
    • READ_COMMITTED:允许读取已提交的数据,避免脏读。
  • timeout:事务超时时间(秒),如果事务执行超过该时间,自动回滚。默认值为 -1(永不超时)。
  • rollback-for/no-rollback-for:指定哪些异常触发回滚 / 不触发回滚。默认情况下,运行时异常(RuntimeException)会回滚,受检异常(如 IOException)不会回滚。
  1. AOP织入事务

事务通知定义了事务规则,但需要通过 AOP 织入到目标方法中才能生效。AOP 负责将事务逻辑与业务方法关联起来。

<!--配置aop织入事务-->
<aop:config>
    <!-- 定义切入点:哪些类的哪些方法需要应用事务 -->
    <aop:pointcut id="txPointcut" expression="execution(* com.pp.dao.*.*(..))"/>
    <!-- 将事务通知织入到切入点 -->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>

现在所有方法都配置了事务了

posted @ 2025-09-09 17:51  pp学编程  阅读(8)  评论(0)    收藏  举报