Spring学习笔记,包含Spring IOC、AOP基本原理、Bean管理、Spring 事务等等

😀😀😀创作不易,各位看官点赞收藏.

Spring 基础笔记

Spring:是一个开源框架,它由[Rod Johnson](https://baike.baidu.com/item/Rod Johnson)创建。它是为了解决企业应用开发的复杂性而创建的。Spring 使用基本的 JavaBean 来完成以前只可能由EJB完成的事情。Spring 的用途不仅限于服务器端的开发,从简单性、可测试性和松耦合的角度而言,任何 Java 应用都可以从 Spring 中受益。

Spring官网:https://spring.io/

Spring 的7大模块:

image-20210728135929929

  • 核心容器(Spring Core):核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转 (IOC)模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。
  • Spring的上下文(Spring Context):Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能。
  • Spring AOP:通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中。
  • Spring DAO:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。
  • Spring ORM:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。
  • Spring WEB:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。
  • Spring WEB MVC:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。

1、控制反转 (IOC)

1.1、IOC 底层原理

IOC 底层主要使用的是 xml 文件解析、工厂模式、反射来实现的。IOC 思想就是基于 IOC 容器,IOC 容器底层就是一个对象工厂,是一种思想并不是一种技术。

  • 配置 xml 文件,配置对象的相关信息。
<bean id="user" class="com.entity.User"></bean>
  • 创建工厂类来创建对象。
class Factory{    
    public static User getUser(){
        // 进行xml解析,获取类的全限定类名(class属性对应的值)
        String clazzValue = .....;
        Class clazz = Class.forName(clazzValue); // 通过反射获取对象的Class对象
        return (User)clazz.newInstance(); // 通过反射,无参构造器来创建一个对象
    }
}

IOC 工厂的两个接口:BeanFactory、ApplicationContext。这两个接口的作用都可以通过加载配置文件,通过工厂这个过程去创建对象。

  • BeanFactory:IOC 最基本的实现方式,是 Spring 内部使用的接口一般不提供给开发人员使用。
@Test
public void test1(){
    // 它在加载配置文件时不会创建对象,它使用的是懒汉式加载,在使用某个对象的时候才会去创建这个对象
    BeanFactory applicationContext = new ClassPathXmlApplicationContext("services.xml");

    // 创建并获取对象
    UserDaoImpl impl = applicationContext.getBean("userDaoImpl", UserDaoImpl.class);
}
  • ApplicationContext:是 BeanFactory 的子接口,提供了更多功能更加强大,面向开发人员使用。
@Test
public void test2(){
    // 它在加载配置文件时就创建了对象,采用的是饿汉式
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("services.xml");

    // 获取对象
    UserDaoImpl impl = applicationContext.getBean("userDaoImpl", UserDaoImpl.class);
}

​ 它有两个重要的实现类:FileSystemXmlApplicationContext、ClassPathXmlApplication 这两个类都是用于加载配置文件的,就是加载配置文件的路径不同。前者加载文件路径必须是绝对路径,而后者加载文件路径是相对于 src 路径下的。

1.2、IOC 之Bean管理 ( XML )

Bean管理:在Spring容器中Bean管理包含两个步骤,Spring 创建对象、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">
    
    <!-- 在Spring的配置文件中,通过bean标签就可以创建一个对象,通过对应类的无参构造器创建对象-->
    <!-- bean标签常用属性:
                        id:创建对象的唯一标识,后面通过这个标识获取这个对象
                        class:对象的全路径名,用于创建对象
                        name:作用和id一样,区别就是id不能加特殊符号,name可以特殊符号
    -->
    <bean id="user1" class="com.xiaotanke.entity.User" ></bean>
</beans>
  • 属性 Set 方法注入:
// 设置类的属性和属性的set方法
public class User {
    private String name;
    private int age;
    private boolean isMan;
    private List<String> hobbies;
    private String[] array;
    private Map<String,String> map;
    private User user;
    private Properties properties;

    public void setProperties(Properties properties) {this.properties = properties;}
    public void setUser(User user) {this.user = user;}
    public void setArray(String[] array) {this.array = array;}
    public void setName(String name) { this.name = name;}
    public void setAge(int age) {this.age = age;}
    public void setMan(boolean man) {isMan = man;}
    public void setHobbies(List<String> hobbies) {this.hobbies = hobbies;}
    public void setMap(Map<String, String> map) {this.map = map;}
}
<?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="user1" class="com.xiaotanke.entity.User" >
        <property name="name" value="zhangsan"/>  <!-- 普通类型注入-->
        <property name="age" value="10"/>
        <property name="man" value="true"/>
        <property name="hobbies">                   <!-- List属性注入-->
            <list>
                <value>参数一</value>
                <value>参数二</value>
                <value>参数三</value>
            </list>
        </property>
        <property name="array">                    <!-- 数组属性注入-->
            <array>
                <value>值1</value>
                <value>值2</value>
                <value>值3</value>
            </array>
        </property>
        <property name="map">                      <!-- map属性注入-->
            <map>
                <entry key="key1" value="value1"/>
                <entry key="key2" value="value2"/>
            </map>
        </property>
        <property name="user" ref="user2"/>        <!-- 引用属性注入,引用已经创建的对象-->
        <property name="properties">               <!-- properties属性注入-->
            <props>
                <prop key="学号">001</prop>
                <prop key="身份证号">002</prop>
                <prop key="姓名">张三</prop>
            </props>
        </property>
    </bean>

    <bean id="user2" class="com.xiaotanke.entity.User"/>
</beans>
  • 有参构造器注入,需要类中添加一个有参构造器,通过有参构造器创建对象
<!-- 有参构造注入参数需要和类中的有参构造器相互对应-->
<!-- constructor-arg有三种指定属性的方法:
                name:根据属性的名称指定
                index:根据构造器中参数的顺序指定
                type:根据参数的类型指定(不用)
-->
<bean id="user3" class="com.xiaotanke.entity.User">
    <constructor-arg name="name" value="张三"/>
    <constructor-arg name="age" value="10"/>
    <constructor-arg name="isMan" value="true"/>
    <constructor-arg name="hobbies">
        <list>
            <value>参数一</value>
            <value>参数二</value>
            <value>参数三</value>
        </list>
    </constructor-arg>
    <constructor-arg name="array">
        <array>
            <value>值1</value>
            <value>值2</value>
            <value>值3</value>
        </array>
    </constructor-arg>
    <constructor-arg name="map">
        <map>
            <entry key="key1" value="value1"/>
            <entry key="key2" value="value2"/>
        </map>
    </constructor-arg>
    <constructor-arg name="user" ref="user1"/>
    <constructor-arg name="properties">
        <props>
            <prop key="姓名">张三</prop>
        </props>
    </constructor-arg>
</bean>
  • p命名空间注入属性,实质上是Set方法注入,为了简化xml配置
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"  
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">  <!-- 这里需要加上p命名约束,从上面复制-->
    
    <!-- 对于基本类型可以使用p命名空间注入,对于复杂类型使用set方法注入-->
    <!-- p:user-ref="user1" 引用一个spring中已经存在的bean-->
    <bean id="user4" class="com.xiaotanke.entity.User" p:name="张三" p:age="10" p:user-ref="user1"/>
</beans>    
  • 特殊值的注入,在注入的值中存在特殊符号,导致xml解析出现问题
<bean id="user5" class="com.xiaotanke.entity.User">
    <property name="name">
        <value>
            <![CDATA[含有特殊符号值]]>
        </value>
    </property>
</bean>
<!-- 注入集合中是对象类型-->
<bean id="user6" class="com.xiaotanke.entity.User">
    <property name="users">
        <list>
            <ref bean="user1"/>  <!-- 通过ref引入外部bean对象-->
            <ref bean="user2"/>
            <ref bean="user3"/>
        </list>
    </property>
</bean>

1.3、IOC 之Bean管理 (FactoryBean)

​ 在Spring中有两种类型的bean,普通bean和工厂bean (FactoryBean)。

<!-- 普通bean,创建对象时,指定是什么类型的对象,获取就是这类对象-->
<bean id="user" class="com.xiaotanke.entity.User"/>

工厂bean:配置一种类型,但是在返回的时候可以返回与配置不同的其它类型的对象。需要在类中实现FactoryBean接口,并重写接口的方法。

public class Student implements FactoryBean<User> {

    /**
     * 设置这个类再获取时的类型
     */
    @Override
    public User getObject() throws Exception {
        return new User();
    }

    /**
     * 获取时对象的Class对象
     */
    @Override
    public Class<?> getObjectType() {
        return User.class;
    }

    /**
     * 设置对象在Spring容器中是否单例
     */
    @Override
    public boolean isSingleton() {
        return FactoryBean.super.isSingleton();
    }
}
<!-- 创建一个student对象-->
<bean id="s" class="com.xiaotanke.entity.Student"/>
public static void main(String[] args) {
    ApplicationContext context =
            new ClassPathXmlApplicationContext("bean.xml");
    // 从Spring容器中获取s这个student对象时,它返回的是一个User类型的对象,说明这是一个工厂bean
    User s = context.getBean("s", User.class);
    System.out.println(s);
}

1.4、Bean的作用域

​ 在Spring中可以设置创建对象是单实例和多实例,默认情况下是单例 bean。

image-20210730173344601

单例对象:在Spring中只创建了一个对象,全局上下文中获取都是同一个对象,在容器初始化时就会创建对象。(默认是单例对象)

image-20210730174524038

<!-- 铜鼓scope属性创建一个单例对象,可以省略-->
<bean id="user101" class="com.xiaotanke.entity.User" scope="singleton"/>
public static void main(String[] args) {
    ApplicationContext context =
            new ClassPathXmlApplicationContext("bean.xml");
    User user1 = context.getBean("user101", User.class);
    User user2 = context.getBean("user101", User.class);
    System.out.println(user1 == user2); // 输出为true,说明两个对象是相同的
}

原型对象:从Spring容器中获取对象时,会新创建一个对象并返回,这个对象是在获取对象是才会创建。

image-20210730175428416

<!-- 铜鼓scope属性创建一个原型(多例)对象-->
<bean id="user101" class="com.xiaotanke.entity.User" scope="prototype"/>
public static void main(String[] args) {
    ApplicationContext context =
        new ClassPathXmlApplicationContext("bean.xml");
    User user1 = context.getBean("user101", User.class);
    User user2 = context.getBean("user101", User.class);
    System.out.println(user1 == user2); // 输出为false,说明获取对象时新创建了一个对象
}

注意事项:

  • 单例对象是在加载配置文件的时候就创建了这个对象,后面获取到的都是这个对象。
  • 多例对象是在通过content上下文获取对象时才会创建一个新的对象。
  • request、session 等其它作用域,在加载配置文件中会创建对应的对象并把对象放在对应的域中,request对象会放在Web请求的request域中。

1.5、Bean的生命周期

​ 在Spring中,Bean的生命周期就是对象的创建到对象的销毁的这个过程,主要包含几个步骤。

  • 创建对象:通过配置文件创建一个对象(无参构造器创建对象)。
<!-- 创建对象-->
<bean id="admin" class="com.xiaotanke.entity.Admin"></bean>
  • 通过 set 注入给对象属性赋值。
<bean id="admin" class="com.xiaotanke.entity.Admin">
    <property name="name" value="张三"/>
</bean>
  • 初始化对象,需要自己设置一个初始化方法,然后通过 init-method 属性指定一个初始化方法。
public void initMethod(){
    System.out.println("3、初始化方法");
}
<bean id="admin" class="com.xiaotanke.entity.Admin" init-method="initMethod">
    <property name="name" value="张三"/>
</bean>
  • 从Spring容器中获取并使用对象。
  • 关闭容器,并销毁Spring容器中的所有bean对象,通过 destroy-method 指定一个销毁方法。需要手动关闭容器才会执行对应bean的销毁方法。
public void destroyMethod(){
    System.out.println("5、销毁bean对象");
}
<!-- 创建对象-->
<bean id="admin" class="com.xiaotanke.entity.Admin" init-method="initMethod" destroy-method="destroyMethod">
    <property name="name" value="张三"/>
</bean>

初始化对象的后置处理器:创建一个类实现 BeanPostProcessor 接口并重写接口方法,这是在初始化对象前后会执行对应的方法。将这个类放到Spring容器中,在这个容器中的所有对象在初始化前后都会执行对应的方法,处处理器需要放到 Spring 容器中才会生效。

public class MyBeanPostProcessor implements BeanPostProcessor {

    // 初始化前执行的方法
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

        System.out.println("初始化bean对象前执行方法");

        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    // 初始化后执行的方法
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        System.out.println("初始化bean对象后执行方法");

        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}

bean的生命周期:

  • 通过无参构造器创建对象
  • 通过set方法给对象属性赋值
  • 对象初始化前
  • 对象通过初始化方法进行初始化
  • 对象初始化后
  • 使用创建好的对象
  • 关闭容器,销毁对象

1.6、Bean的自动装配

<bean id="myBeanPostProcessor" class="com.xiaotanke.hander.MyBeanPostProcessor"/>

<!-- Emp中有一个属性名dept的Dept类型的属性-->
<!-- 可以通过autowire属性来设置属性的自动装配,有两种方式:byName和byType
                byName:将Spring容器中对象名称与当前属性相同的bean对象自动注入个这个属性。
                byType:根据属性的类型在Spring容器中找相同类型的bean对象,然后注入(那么同一种类型的类就只能有一个容器bean),如果容器中有多个类型匹配就会报错,如果没有一个类型匹配就会返回 null
-->
<bean id="emp1" class="com.xiaotanke.autowire.Emp" autowire="byName"/>

<bean id="emp2" class="com.xiaotanke.autowire.Emp" autowire="byType"/>
<bean id="dept" class="com.xiaotanke.autowire.Dept"/>

引入外部文件:通过引入外部文件来配置bean对象的属性。

<?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:p="http://www.springframework.org/schema/p"
       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">
    <!-- 添加一个context约束-->
    
    <!-- 引入外部文件,location是相对于resources目录下-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    
    <!-- 读取配置文件,并进行属性注入-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${driverClassName}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${user}"/>
        <property name="password" value="${password}"/>
    </bean>
</beans>
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306
user=root
password=1234567

1.7、IOC 之Bean管理 (注解)

创建对象:使用注解创建对象并把对象放在Spring容器中,下面是几个常见的注解。

/**
 * @Component 创建一个普通bean对象
 * @Controller 创建一个前端控制器对象
 * @Servic 创建一个服务层的对象
 * @Repository 创建一个dao层的对象
 * 
 *  它们的功能都是一样的,都是创建一个对象,只是使用的场景不一样
 */

注意事项:

  • 使用注解前需要引入spring-aop这个依赖。
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.3.20</version>
</dependency>
  • 使用注解需要开启组件扫描。
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       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">
    
   	<!-- 引入context约束-->
    
    <!-- 开启组件注解扫描,base-package属性指定扫描的包,在这个包下的所有组件注解都会加载如果存在多个包,使用英文逗号隔开
	-->
    <context:component-scan base-package="com.xiaotanke"/>  

</beans>

获取对象并使用

// 创建了一个对象,对象的名称默认是类名称的首字母小写,也可以通过value属性来指定一个对象的名称
@Component(value = "test")
public class ComponentTest {
}
public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
    // 获取对象
    ComponentTest componentTest = context.getBean("test", ComponentTest.class);
    System.out.println(componentTest);
}

配置组件扫描

<!-- use-default-filters:表示关闭自动扫描,不会全部扫描这个包下的内容-->
<context:component-scan base-package="com" use-default-filters="false">
    <!-- 配置扫描的注解,在这个包下有Component注解的类才会去扫描-->
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
    <!-- 在这个包下的Repository注解的类不会去扫描-->
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan>

1.8、注解实现属性注入

简单属性注入:在属性名前面使用Value注解,也可以在set方法上使用Value注解。一般属性简单使用注解,属性复杂还是使用xml配置。

@Component
public class Admin {
    @Value("张三")
    private String name;
}

引用属性注入:将已经在Spring容器的对象赋值给某个属性,有两种方式。

  • @Autowired方式:
@Controller
public class ControllerTest {

     /**
     * ‘@Autowired’:可以标注在属性上、方法上和构造器上,来完成自动装配。
     *              默认是根据属性类型,spring自动将匹配到的属性值进行注入
     *              当标注的属性是接口时,其实注入的是这个接口的实现类, 如果这个接口有多个实现类,(会先按照类型注入,存在多种类型就会按照名称注入)
     *              只使用@Autowired就会报错,因为它默认是根据类型找,然后就会找到多个实现类bean,
     *              所有就不知道要注入哪个。然后它就会根据属性名去找。
     *              所以如果有多个实现类可以配合@Qualifier(value=“类名”)来使用
     */
    @Autowired
    @Qualifier(value = "serviceTest")	// Qualifier需要和Autowired一同使用
    private ServiceTest serviceTest;

    /**
     * 在方法上使用,会注入到参数上
     */
    @Autowired
    @Qualifier(value = "serviceTest")
    public void test1(ServiceTest serviceTest){
    }
}
  • @Resource方式:
@Service
public class ServiceTest {

    /**
     * 这个注解数java的注解,它是按照名称进行注入,如果名称相同就会按照类型注入(先名称后类型)
     * 可以通过name属性来指定注入对象的名称
     */
    @Resource(name = "repositoryTest")
    private RepositoryTest repositoryTest;
}

Spring中的配置类:用于替代Spring的配置文件,可以通过这个配置类来实现对象的创建等功能。

// 使用这个注解表示是一个配置类,会被Spring容器识别
@Configuration
// 这个注解是配置组件的扫描包
@ComponentScan(basePackages = {"com.xiaotanke.annotation","com.xiaotanke.component"})
public class SpringConfig {

    /**
     * 这个注解是创建了一个对象
     * bean的名称就是带注释的方法的名称。如果指定,则忽略方法名
     */
    @Bean 
    public ControllerTest controllerTest(){
        return new ControllerTest();
    }
}
public static void main(String[] args) {
    // 加载配置类,与加载配置文件作用一样,参数是配置类的Class对象
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
    ControllerTest controllerTest = context.getBean("controllerTest", ControllerTest.class);
    System.out.println(controllerTest);
}

1.9、配置类开启组件扫描

可以通过配置类去开启组件扫描,不用再创建一个 xml 文件去开启组件扫描。

// 标志是一个配置类
@Configuration
// 需要扫描的包,可以配置多个
@ComponentScan({"com.jx","com"})
public class SpringConfig {
}

获取组件:

public static void main(String[] args) {
    // 加载配置类
    ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
    // 获取组件 。。。。
}

2、切面编程 (AOP)

2.1、AOP 底层原理

AOP 的底层使用的动态代理实现的,有两种情况的动态代理(JDK原生动态代理、CGLIB动态代理)。

2.1.1、JDK 原生动态代理

JDK 动态代理的是代理接口,代理对象和被代理对象实现同一个接口,然后通过代理对象完成被代理对象需要完成的任务并加入自己的逻辑任务。

// 被代理对象实现的接口
public interface Rent {
    void rent();
}
// 被代理对象
public class RentServiceImpl implements Rent{
    @Override
    public void rent() {
        System.out.println("我需要租房!!!!");
    }
}
// 代理对象生成类
public class ProxyFactory implements InvocationHandler {

    // 被代理对象
    private Object target;
    public void setTarget(Object target) {
        this.target = target;
    }
    // 生成代理对象
    public Object newInstanceProxy(){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
    }
    // 对被代理对象做增强
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理类前置增强");

        // 执行被代理对象执行的方法
        Object result = method.invoke(this.target, args);

        System.out.println("代理类后置增强");
        return result;
    }
}
// 测试
public static void main(String[] args) {
    // 被代理的接口
    Rent service = new RentServiceImpl();
    
    // 代理对象工厂类
    ProxyFactory factory = new ProxyFactory();
    // 设置被代理对象接口
    factory.setTarget(service);

    // 生成接口代理对象
    Rent proxy = (Rent) factory.newInstanceProxy();
    // 代理对象执行对应的方法
    proxy.rent();
}
2.1.2、CGLIB 动态代理

CGLIB动态代理基本原理是使用子类去继承被代理类或者接口,然后通过子类去对被代理类进行增强。

// 被代理对象类
public class RentServiceImpl {
    public void rent(){
        System.out.println("我要租房子!!!");
    }
}
// CGLIB 代理工厂类
public class CGLIBFactory implements MethodInterceptor {

    // 被代理对象
    private Object target;

    public void setTarget(Object target) {
        this.target = target;
    }

    // 产生动态代理对象
    public Object newInstance(){
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    // 代理增强

    /**
     * @param o 代理对象
     * @param method 被代理的方法
     * @param objects 参数
     * @param methodProxy 代理方法
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("前置增强");

        // 执行代理方法
        Object result = methodProxy.invokeSuper(o, objects);

        System.out.println("后置增强");
        return result;
    }
}
public static void main(String[] args) {
    // 生成代理对象工厂
    CGLIBFactory factory = new CGLIBFactory();

    // 被代理对象
    RentServiceImpl rentService = new RentServiceImpl();

    factory.setTarget(rentService);
    RentServiceImpl o = (RentServiceImpl) factory.newInstance();
    o.rent();
}

2.2、AOP 基本操作

/**
 * 1、连接点:可以被增强的方法
 * 2、切入点:实际被增强的方法
 * 3、通知(增强):增强的逻辑方法,通知有多种类型(前置通过、后置通知、环绕通知、异常通知、最终通知)
 *         前置通知:在切入点方法之前执行
 *         后置通过:在切入点方法执行完毕后执行
 *         环绕通知:在切入点方法执行的前后都会执行
 *         异常通知:在切入点方法出现异常的时候会执行
 *         最终通知:这个无论怎样都会执行,与finally类似
 * 4、切面:是一个动作,把通知用在切入点的过程
*/
2.2.1、基于注解 AspectJ
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.3.20</version>
</dependency>
  • 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 配置aop和context约束-->

    <!-- 开启注解扫描-->
    <context:component-scan base-package="com.xiaotanke"/>
    <!-- 开启AspectJ代理对象生成,如果在扫描组件中有AspectJ注解的都会被看成一个代理对象-->
    <!-- proxy-target-class属性,
            默认为false: 表示使用jdk动态代理织入增强,
            true:表示使用CGLib动态代理技术织入增强
    -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
  • 配置不同类型的通知
/**
 * 增强类
 */
@Component
@Aspect
public class MyAspectj {

    /**
     * execution()切入点表达式
     *      execution(* com.xiaotanke.service.UserServiceImpl.*(..))
     *         *:表示修饰符,所有的修饰符和返回类型
     *         com.xiaotanke.service:切入点的全限定路径
     *         UserServiceImpl:切入点类
     *         *(..):切入的方法及其参数列表
     */

    /**
     * 前置通知:在被增强方法之前前执行
     */
    @Before(value = "execution(* com.xiaotanke.service.UserServiceImpl.*(..))")
    public void before(){
        System.out.println("添加user方法的前置通知");
    }

    /**
     * 后置通知:无论如何都会执行的方法,在方法执行后执行
     */
    @After(value = "execution(* com.xiaotanke.service.UserServiceImpl.*(..))")
    public void after(){
        System.out.println("添加user方法后的后置通知");
    }

    /**
     * 环绕通知:被增强方法的前后执行
     * @param joinPoint 被增强方法信息接口,通过这个参数可以获取被增强方法信息
     * @throws Throwable 异常
     */
    @Around(value = "execution(* com.xiaotanke.service.UserServiceImpl.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("添加user方法的前置环绕通知");

        // 执行方法
        Object result = joinPoint.proceed();

        System.out.println("添加user方法的后置环绕通知");
        return result; // 返回执行结果
    }

    /**
     * 返回通知:增强方法执行后执行,能获取方法的返回值,returing对应的值和参数值名称相同
     */
    @AfterReturning(value = "execution(* com.xiaotanke.service.UserServiceImpl.*(..))",returing="result")
    public void afterReturning(Object result){
        System.out.println("添加user方法的返回通知"+result);
    }

    /**
     * 异常通知:当被增强方法出现异常时执行,但是后面的最终通知也会继续执行,可以获取目标的异常信息
     *			throwing属性值和参数名称需要相同
     */
    @AfterThrowing(value = "execution(* com.xiaotanke.service.UserServiceImpl.*(..))",throwing="exception")
    public void afterThrowing(Throwable exception){
        System.out.println("添加user方法的异常通知"+exception);
    }
}
public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
    UserServiceImpl userServiceImpl = context.getBean("userServiceImpl", UserServiceImpl.class);
    // 执行被增强方法
    userServiceImpl.add();
}

未出现异常时的执行顺序:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TVILQXlX-1690514070885)(https://s2.loli.net/2022/07/30/1EHtk5CPfzL2cv9.png)]

出现异常时的执行顺序:最终通知始终会执行的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jsP4pTHA-1690514070885)(https://s2.loli.net/2022/07/30/HLbJ4kuZsncxaqF.png)]

抽取公共的切入点:

/**
	使用一个方法来作为公共的切入点
*/
@Pointcut(value = "execution(* com.xiaotanke.service.UserServiceImpl.*(..))")
public void point(){}

// 通知直接可以使用这个方法来引用公共的切入点
@Before(value = "point()")
public void before(){
    System.out.println("添加user方法的前置通知");
}

对个增强类对同一个方法进行增强,可以设置增强方法的优先级:

/**
 * 设置增强的优先权,整数数值越小,优先级越高,
 *  前置通知优先级越高越先执行
 *  后置通知是优先级越高,越后执行
 */
@Component
@Aspect
@Order(1)  
public class AspectjTwo {
    @Before(value = "execution(* com.xiaotanke.service.UserServiceImpl.*(..))")
    public void before(){
        System.out.println("添加user方法的前置通知(优先级一)");
    }

    @Around(value = "execution(* com.xiaotanke.service.UserServiceImpl.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("添加user方法的前置环绕通知(优先级一)");

        // 执行方法
        Object result = joinPoint.proceed();

        System.out.println("添加user方法的后置环绕通知(优先级一)");
        return result; // 返回执行结果
    }
}
2.2.2、基于配置文件AspectJ

通过配置文件来实现AspectJ:

<!-- 创建对象-->
<bean id="userServiceImpl" class="com.test3.UserServiceImpl"/>
<bean id="userServiceProxy" class="com.test3.UserServiceProxy"/>

<!-- 配置aop增强-->
<aop:config>
    <!-- 切入点-->
    <aop:pointcut id="point" expression="execution(* com.test3.UserServiceImpl.add(..))"/>
    <!-- 配置切面,ref引用增强类-->
    <aop:aspect ref="userServiceProxy">
        <!-- 配置对应的通知,method:增强类的方法名,pointcut-ref:增强的切入点-->
        <aop:before method="before" pointcut-ref="point"/>
        <aop:after-returning method="afterReturning" pointcut-ref="point"/>	
    </aop:aspect>
</aop:config>

测试:

public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
    UserServiceImpl service = context.getBean("userServiceImpl", UserServiceImpl.class);
    service.add();
}

3、Spring 事务

事务:在一组逻辑业务中,要么都成功,要么都失败。如果有一个步骤失败了,其他成功是步骤都要回滚。事务有四个特性:原子性、一致性、隔离性、持久性。

原子性:在操作中不可分割,要么都成功要么都失败
一致性:数据操作之前和操作之后的总量是不变的,数据保持一致
隔离性:两个事务去操作同一条数据,他们之间不会产生影响的
持久性:数据会永远持久化保存数据库中

在Spring中,分为编程式事务和声明式事务,一般在开发中都使用声明式事务。

编程式事务:手动开启、提交、回滚事务。

声明式事务:底层通过AOP方式在方法前使用编程式事务的方法开启事务,在方法后提交或回滚。用配置文件的方法或注解方法控制事务。

3.1、声明式事务

​ 在Spring中的事务管理API中,PlatformTransactionManager接口配置了不同框架的事务管理,通过接口的不同实现类去进行事务的管理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VzDVaJ6f-1690514070886)(https://s2.loli.net/2022/08/02/Dg6wmeIs5yXjMWZ.png)]

基于注解:

  • 集成mybatis到Spring中:
<!-- 注解扫描-->
<context:component-scan base-package="com.xiaotanke"/>

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&amp;serverTimezone=UTC"/>
    <property name="username" value="root"/>
    <property name="password" value="1234567"/>
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <property name="mapperLocations" value="classpath:com/xiaotanke/mapper/*.xml"/>
</bean>

<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
  • 创建事务管理器:
<!-- 注解扫描-->
<context:component-scan base-package="com.xiaotanke"/>

<!-- 创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.support.JdbcTransactionManager"/>

<!-- 引入aop和tx约束-->
<!-- 开启事务-->
<tx:annotation-driven transaction-manager="transactionManager"/>
  • @Transactional注解,可以作用在类上表示类中所有方法都加上了事务,可以作用在方法上表示这个方法加上了事务。加上事务的方法表示如果方法中出现了异常或者不正常中断,那么方法之前已经完成的任务就会回滚到初始状态。
@Transactional
public boolean transfer(Integer money) {
    UserMapper userMapper = sqlSessionTemplate.getMapper(UserMapper.class);
    // 查询到user
    User user1 = userMapper.query(101);
    User user2 = userMapper.query(102);
    // 开始转账
    if (action(user1,user2,100)) {
        userMapper.update(user1);
        System.out.println(1/0);  // 出现异常,之前更新的user1的数据就会回滚
        userMapper.update(user2);
        return true;
    }else {
        System.out.println("转账失败,余额不足");
    }
    return false;
}

@Transaction注解常见属性:

  • readOnly:指定这个方法只允许进行查询操作,默认值 false。
  • timeout:超时时间,超过这个时间事务自动回滚,默认值 -1 表示永不超时。
  • rollbackFor:值为 Class 数组,对于出现哪些异常时进行回滚操作。
  • noRollbackFor:值为 Class 数组,出现这些异常信息依然不回滚。
  • rollbackForClassName:值为字符串数组,异常类权限定名称,如果出现指定异常就会回滚。
  • noRollbackForClassName:值为字符串数组,异常类权限定名称,出现这些异常信息依然不回滚。
  • propagation:指定事务的传播行为,默认 Propagation.REQUIRED
  • isolation:指定事务隔离级别,默认值 Isolation.DEFAULT,使用数据库默认隔离级别。

基于xml方式:创建事务管理器,配置事务通知,通过aop切入到方法上。

  • 创建事务管理器
<!-- 创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<!-- 引入aop和tx约束-->
<!-- 开启事务-->
<tx:annotation-driven transaction-manager="transactionManager"/>
  • 配置事务通知:
<!-- 配置事务通知-->
<tx:advice id="advice">
    <tx:attributes>
        <!-- 指定方法名上加上事务,可以使用通配符,以insert开头的所有方法-->
        <!-- 这里面也可以设置对应的隔离级别和传播特性以及其它属性-->
        <tx:method name="insert*"/>
    </tx:attributes>
</tx:advice>
  • AOP将事务通知进行切入:
<!-- aop进行事务切入-->
<aop:config>
    <!-- 配置切入点-->
    <aop:pointcut id="txPoint" expression="execution(* com.xiaotanke.service.impl.UserServiceImpl.*(..))"/>
    <!-- 配置切面:
                advice-ref:事务通知
                pointcut-ref:切入点
    -->
    <aop:advisor advice-ref="advice" pointcut-ref="txPoint"/>
</aop:config>

3.2、事务传播行为

事务传播行为:指的是在一个存在事务的方法被另一个存在事务的方法调用时,事务进行处理。在Spring中一共有7种传播行为。

/**
 * 事务的传播行为:
 *      1、Propagation.REQUIRED(默认):在事务A方法1中去调用事务B方法2,那么方法2会加入到方法1的事务中,执行方法1的事务。
 *      2、Propagation.REQUIRES_NEW:在事务A的方法1中去调用事务B方法2,那么方法1的事务作为外层事务,方法2的事务作为内层事务
 *                                  方法2会创建一个自己的事务,两个事务不会干扰,如果方法2执行成功,但是方法1执行失败,方法2													依然可以进行提交,方法1就会执行回滚操作。
 *     3、Propagation.SUPPORTS:在事务A的方法1中去调用事务B的方法2,如果在方法1中有事务在运行,那么方法2也会加入到方法1的事													务,如果方法1中没有事务,那么方法2就支持事务。
 */

@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean transfer(Integer money) {}

3.3、事务隔离级别

​ 事务之间可以同时操作一条或多条数据,如果不考虑事务的隔离级别就会出现脏读、不可重复读、幻读问题。通过设置事务的隔离级别来解决对应的问题。

脏读:一个未提交的事务读取到另一个未提交事务的数据。如果一个事务进行了事务回滚,那么另一个事务读取到的数据就是脏数据。

不可重复读:一个未提交的事务读取到一个已提交事务的数据,导致事务每次读取到的数据不一样。

幻读:一个事务在前后两次查询的结果不一样。

/**
 * 事务隔离级别:
 *      1、Isolation.READ_UNCOMMITTED:读未提交,存在脏读、不可重复度、幻读问题
 *      2、Isolation.READ_COMMITTED:读已提交,存在不可重复度、幻读问题
 *      3、Isolation.REPEATABLE_READ:可重复读,存在幻读问题
 *      4、Isolation.SERIALIZABLE:序列化,一个事务未完成时,另一个事务需要进行等待,不存在问题。
 *		5、Isolation.DEFAULT:使用数据库默认隔离级别,默认值
 */
// 默认的隔离级别是存储方式默认隔离界别,mysql默认隔离级别是可重复读
@Transactional(propagation = Propagation.NOT_SUPPORTED,isolation = Isolation.SERIALIZABLE)
public boolean transfer(Integer money) {}

4、Spring5 新特性

4.1、日志框架

​ Spring5中已经移除了Log4jConfigListener,官方建议使用Log4j2。

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.18.0</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.18.0</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.18.0</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.36</version>
</dependency>
  • 创建一个log4j2.xml文件,并编写文件
<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration monitorInterval="5">
    <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->

    <!--变量配置-->
    <Properties>
        <!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
        <!-- %logger{36} 表示 Logger 名字最长36个字符 -->
        <property name="LOG_PATTERN" value="%date{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
        <!-- 定义日志存储的路径 -->
        <property name="FILE_PATH" value="E:\code\idea\study\ssm_L\Spring2.0\Spring-Transaction" />
        <property name="FILE_NAME" value="Spring-Transaction" />
    </Properties>

    <appenders>
        <console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式-->
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
        </console>

        <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定-->
        <File name="FileLog" fileName="${FILE_PATH}/${FILE_NAME}.log" append="true">
            <PatternLayout pattern="${LOG_PATTERN}"/>
        </File>
    </appenders>

    <!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
    <!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>

        <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
        <logger name="org.mybatis" level="info" additivity="false">
            <AppenderRef ref="Console"/>
        </logger>
        <!--监控系统信息-->
        <!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。-->
        <Logger name="org.springframework" level="all" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>

        <root level="all">
            <appender-ref ref="Console"/>
            <appender-ref ref="FileLog"/>
        </root>
    </loggers>
</configuration>

4.2、单元测试

​ 在传统Juit中需要每次都加载配置文件,我们可以将Junit配置到Spring中,这样就可以直接注入Spring容器中的对象。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.22</version>
</dependency>

测试类:

/**
 * 整合Junit4
 */
@RunWith(SpringJUnit4ClassRunner.class)  // 单元测试的版本
@ContextConfiguration("classpath:application.xml")  // 加载配置文件
public class JunitTest {
    /**
     * 可以直接注入
     */
    @Autowired
    private UserService userServiceImpl;

    @Test
    public void test1(){
        userServiceImpl.transfer(100);
    }
}

5、Spring 资源操作

Spring Resource:提供低级别资源的访问能力,例如获取项目的根路劲等等操作。

Resource接口:是 Spring 提供的资源访问策略,它本身不提任何资源访问实现,但是有很多实现类去实现了对应的方法。例如:UrlResource、ClassPathResource、FileSystemResource、ServletContextResource、InputStreamResouce、ByteArrayResource。

  • UrlResource 实现类:用来访问网络资源的实现类,支持 URL 绝对路劲。
public static void main(String[] args) throws IOException {
    String urlPath = "https://photo.16pic.com/00/93/69/16pic_9369314_b.png?imageView2/0/format/jpg";
    Resource urlResource = new UrlResource(urlPath);
    // 获取相关信息
    System.out.println("URL=> " + urlResource.getURL());
    System.out.println("fileName=> " + urlResource.getFilename());
    System.out.println("描述信息=> " + urlResource.getDescription());

    // 文件流
    InputStream inputStream = urlResource.getInputStream();
    FileOutputStream outputStream = new FileOutputStream("test.png");
    byte[] buffer = new byte[1024];
    while (inputStream.read(buffer) != -1){
        outputStream.write(buffer);
    }
}
  • ClassPathResource:访问类路径下的资源,可以自动搜索位于 class 路劲下资源文件。
public static void main(String[] args) throws IOException {
    // 文件存放到项目的路劲下
    String path = "file/test.txt";
    Resource resource = new ClassPathResource(path);
    System.out.println("fileName=> " + resource.getFilename());
    System.out.println("描述信息=> " + resource.getDescription());
    InputStream inputStream = resource.getInputStream();
    byte[] bytes = new byte[1024];
    while (inputStream.read(bytes) != -1){
        System.out.println(new String(bytes));
    }
}
  • FileSystemResource:访问系统文件资源。
public static void main(String[] args) throws IOException {
    // 文件在系统的绝对路径
    String path = "E:\\code\\idea\\projects\\spring-boot-practice\\boot-01\\src\\main\\resources\\file\\test.txt";
    Resource resource = new FileSystemResource(path);
    System.out.println("fileName=> " + resource.getFilename());
    System.out.println("描述信息=> " + resource.getDescription());
    InputStream inputStream = resource.getInputStream();
    byte[] bytes = new byte[1024];
    while (inputStream.read(bytes) != -1){
        System.out.println(new String(bytes));
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值