freyhe

导航

Spring

1.介绍

功能:

1.创建 spring容器
2.把 spring配置文件中的资源放到 spring容器中进行管理
3.把 spring容器放到 servletContext(application作用域)中保存

Spring 是一个基于 IOC[控制反转]和 AOP[面向切面编程]的免费的、开源的、轻量级的、Java 开发框架,是用来简化开发的。
Spring 在软件三层架构中的位置

image-20210716083931324Spring 的模块划分

Spring 主要分成六个模块:

以下就是 Spring 框架的几个主要模块。而在实际的开发过程中,不必要引入所有的 Jar 包,只需要引入与自己项目相关的库就行了。例如,只需要 Spring 框架的容器功能,实现依赖注入,那么只需要引入核Spring 框架是一个分层架构,,它包含一系列的功能要素并被分为大约20个模块。这些模块分为Core Container、Data Access/Integration、Web、AOP(Aspect Oriented Programming)、Instrumentation和测试部分,如下图所示:

image-20210716084207499

image-20210716084230312

image-20210716084301529
Spring 重要知识点梳理
image-20210716084318289

2.IOC

1.含义

全称:Inverse Of Contol:控制反转,指的是将 bean 对象的创建、及对象关联关系的维护由原来我们自己去创建、自己去维护对象之间的关联关系,反转给 spring 的容器来创建对象,及维护对象之间的关联关系。

IOC 作用:解决了上层建筑严重依赖下层建筑的问题,实现了上层建筑对下层建筑的控制。

IOC 底层原理:xml 解析+反射+容器+设计模式//IOC 底层原理:xml 解析、工厂模式、反射
IOC(BeanFactory 接口)

1、IOC 思想基于 IOC 容器完成,IOC 容器底层就是对象工厂

2、Spring 提供 IOC 容器实现两种方式:(两个接口)

(1)BeanFactory:IOC 容器基本实现,是 Spring 内部的使用接口,不提供开发人员进行使用//可以使用,但一般不建议 加载配置文件时候不会创建对象,在获取对象(使用)才去创建对象

(2)ApplicationContext:BeanFactory 接口的子接口,提供更多更强大的功能,一般由开发人员进行使用 加载配置文件时候就会把在配置文件对象进行创建//一般配合Tomcat服务器在Web项目中使用,在加载时就创建对象更好

image-20210716084703104

2.IOC 操作 Bean 管理

1、什么是 Bean 管理

(1)Spring 创建对象(1.获取容器 2.从容器中获取bean对象)

(2)Spirng 注入属性

2、Bean 管理操作有两种方式

(1)基于 xml 配置文件方式实现

(2)基于注解方式实现

3.获取容器的三种方式:

第一种:ClassPathXmlApplicationContext:默认是去 src 目录下找配置文件

ApplicationContext beanFactory = new ClassPathXmlApplicationContext("applicationContext.xml");
Actor bean = beanFactory.getBean(Actor.class);

第二种:FileSystemXmlApplicationContext:默认是去本地磁盘的具体位置去找对应的配置文件

ApplicationContext beanFactory = new FileSystemXmlApplicationContext("D:\\mycode\\springpractice\\day01\\src\\applicationContext.xml");
Actor bean = (Actor) beanFactory.getBean("actor");

第三种:XmlBeanFactory:已废弃

Resource resource = new FileSystemResource("D:\\mycode\\springpractice\\day01\\src\\applicationContext.xml");
BeanFactory beanFactory = new XmlBeanFactory(resource);
Actor bean = (Actor) beanFactory.getBean("actor");

从容器中获取bean对象的两种方式和他们的区别
1. 通过字节码文件获取
2. 通过唯一的id获取(或者name,name可以使用特殊符号比如"/",现在用的不多;id不可以使用特殊符号)

区别:
1. 一个是输入字节码文件 一个是输入字符串的id名
2. 一个是返回自定义类型 一个是返回Object类型
3. 第三如果参数是字节码类型,要求容器中必须有该字节码类型的唯一bean对象如果没有,报错:NoSuchBeanDefinitionException如果容器中有该类型的唯一bean对象,正常获取如果容器中有该类型的多个bean对象,报:NoUniqueBeanDefinitionException

DI:全称:Dependency Injection:依赖注入,说白了,就是给对象的属性赋值。
IOC 是一种控制反转的思想,具体实现就是 DI。

4.分模块配置

将 spring整合其它框架的部门单独拿出来配置一个整合的文件,然后在 spring 框架中通过 import 标签导入其它配置文件信息

<import resource="applicationContext_redis.xml"/>

5.引用内部 bean 和引用外部 bean

外部bean:所有在beans根标签内部直接配置的bean称之为外部bean,外部bean可以被多个bean对象引用。

内部bean:在某一个bean标签内部定义的bean对象,内部bean只能被某个对象的某个属性引用

<bean id="userService" class="com.atguigu.service.UserService"></bean>
<bean id="userServlet01" class="com.atguigu.servlet.UserServlet">
<!-- 1.引用外部bean:可以被任意多个对象引用 -->
    <property name="userService" ref="userService"></property>
</bean>

<bean id="userServlet02" class="com.atguigu.servlet.UserServlet">
    <property name="userService">
    <!--2.内部bean:只能被某个对象的某个属性引用 -->
        <bean id="userService22" class="com.atguigu.service.UserService"></bean>
    </property>
</bean>

6.容器中bean对象的3种创建方式

第一种:构造器[无参构造器、有参构造器]

第二种:静态工厂:是指的通过类的静态方法得到的对象

第三种:实例工厂:是指的通过类对象的普通方法得到的对象

<!--1.无参构造器 -->
<bean class="com.atguigu.bean.Actor"></bean> <!--2.有参构造器 -->
<bean class="com.atguigu.bean.Actor">
    <constructor-arg name="aid" value="1001"/>
    <constructor-arg name="actorName" value="杨紫琼"/>
</bean> 

<!-- 2.静态工厂:是指的通过类的静态方法得到的对象 -->
<bean id="actor" class="com.atguigu.bean.StaticFactory"
      factory-method="getActor"></bean> <!-- 实例工厂:是指的通过类对象的普通方法得到的对象-->
<bean id="instanceFactory" class="com.atguigu.bean.InstanceFactory"></bean>
<bean id="manActor" factory-bean="instanceFactory" factory-method="getManActor"/>

<!-- 3.实例工厂:是指的通过类对象的普通方法得到的对象 -->
<bean id="instanceFactory" class="com.atguigu.bean.InstanceFactory"/>
//先创建InstanceFactory类的对象instanceFactory
<bean id="manActor" factory-bean="instanceFactory"  factory-method="getManActor"/>
//通过调用instanceFactory的getManActor方法来获取manActor对象
image-20210716101726827

7.IOC 操作 Bean 管理(基于xml配置)

1.注入属性

1.字面量

(1)null 值

<!--null 值-->
<property name="address">
    <null/>
</property>

(2)属性值包含特殊符号

<!--属性值包含特殊符号    在 XML 元素中,"<" 和 "&" 是非法的。
    1 把<>进行转义:&lt;&gt; 使用特殊字符的实体名称(HTML ISO-8859-1 参考手册)  
    2 把带特殊符号内容写到 CDATA  使用<![CDATA[ sql 语句 ]]>标记,不被解析器解析
    -->
<property name="address">
    <value><![CDATA[<<南京>>]]></value>
</property>
2.注入属性-外部 bean
<!--1 service 和 dao 对象创建-->
<bean id="userService" class="com.atguigu.spring5.service.UserService">
    <!--注入 userDao 对象
    name 属性:userService类里面属性名称
    ref 属性:创建 userDao 对象 bean 标签 id 值
    -->
    <property name="userDao" ref="userDaoImpl"></property>
</bean>
<bean id="userDaoImpl" class="com.atguigu.spring5.dao.UserDaoImpl"></bean>
3.注入属性-内部 bean

(1)一对多关系:部门和员工
一个部门有多个员工,一个员工属于一个部门
部门是一,员工是多
(2)在实体类之间表示一对多关系,员工表示所属部门,使用对象类型属性进行表示

//部门类
public class Dept {
    private String dname;
      public void setDname(String dname) {
      this.dname = dname;
    }
}
//员工类
public class Emp {
    private String ename;
    private String gender;
    //员工属于某一个部门,使用对象形式表示
    private Dept dept;
    public void setDept(Dept dept) {
        this.dept = dept;
    }
    public void setEname(String ename) {
        this.ename = ename;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }
}

(3)在 spring 配置文件中进行配置

<!--内部 bean-->
<bean id="emp" class="com.atguigu.spring5.bean.Emp">
    <!--设置两个普通属性-->
    <property name="ename" value="lucy"></property>
    <property name="gender" value="女"></property>
    <!--设置对象类型属性-->
    <property name="dept">
        <bean id="dept" class="com.atguigu.spring5.bean.Dept">
            <property name="dname" value="安保部"></property>
        </bean>
    </property>
</bean>
4.注入属性-级联赋值

(1)第一种写法

<!--级联赋值-->
<bean id="emp" class="com.atguigu.spring5.bean.Emp">
    <!--设置两个普通属性-->
    <property name="ename" value="lucy"></property>
    <property name="gender" value="女"></property>
    <!--级联赋值-->
    <property name="dept" ref="dept"></property>
</bean>

<bean id="dept" class="com.atguigu.spring5.bean.Dept">
    <property name="dname" value="财务部"></property>
</bean>

(2)第二种写法

image-20210716103337687
<!--级联赋值-->
<bean id="emp" class="com.atguigu.spring5.bean.Emp">
    <!--设置两个普通属性-->
    <property name="ename" value="lucy"></property>
    <property name="gender" value="女"></property>
    <!--级联赋值-->
    <property name="dept" ref="dept"></property>
                <!--dept内必须有get方法-->
    <property name="dept.dname" value="技术部"></property>
</bean>
<bean id="dept" class="com.atguigu.spring5.bean.Dept">
    <property name="dname" value="财务部"></property>
</bean>
5.注入集合属性

注入属性-内部 给对象的复杂属性赋值(注入)
这里的复杂类型指的是:array、set、list、map、properties 类型的参数
1、注入数组类型属性
2、注入 List 集合类型属性
3、注入 Map 集合类型属性
(1)创建类,定义数组、list、map、set 类型属性,生成对应 set 方法
(2)在 spring 配置文件进行配置
4、在集合里面设置对象类型值

public class ComplexBean {
//提供getter、setter、toString()方法
    private Object[] arr;//属性如果只有一个值,就可以直接使用value或者ref
    private List list;
    private Set set ;
    private Map map;
    private Properties properties;
}

<bean class="com.atguigu.ComplexBean">
    <!-- 1.针对数组类型的属性,用array标签 -->
    <property name="arr">
        <array>
            <value>33</value>
            <bean class="com.atguigu.bean.Book">
                <property name="bid" value="100"/>
                <property name="bookName" value="小红书"/>
                <property name="price" value="100.8"/>
            </bean>
            <ref bean="book01"></ref>
        </array>
    </property>

    <!-- 2.针对list类型的属性,用list标签-->
    <property name="list">
        <list>
            <value>33</value>
            <bean class="com.atguigu.bean.Book">
                <property name="bid" value="100"/>
                <property name="bookName" value="小红书"/>
                <property name="price" value="100.8"/>
            </bean>
            <ref bean="book01"></ref>
        </list>
    </property>

    <!-- 3.针对Set类型的属性,用set标签-->
    <property name="set">
        <set>
            <value>33</value>
            <bean class="com.atguigu.bean.Book">
                <property name="bid" value="100"/>
                <property name="bookName" value="小红书"/>
                <property name="price" value="100.8"/>
            </bean>
            <ref bean="book01"></ref>
        </set>
    </property>

    <!-- 4.针对Map类型的属性,用map标签嵌套entry标签-->
    <property name="map">
        <map>
            <entry key="key1" value="value1"></entry>
            <entry key-ref="book01" value="value2"></entry>
            <entry key="key3" value-ref="book01"></entry>
            <entry key-ref="book01" value-ref="book01"></entry>
        </map>
    </property>

    <!-- 5.针对Properties类型的属性,用Props标签嵌套prop标签-->
    <property name="properties">
        <props>
            <prop key="key1">value1</prop>
            <prop key="key2">value2</prop>
            <prop key="key3">value3</prop>
        </props>
    </property>

5、把集合注入部分提取成为util,可以共用

(1)在 spring 配置文件中引入名称空间 util

(2)使用 util 标签完成 list 集合注入提取

<!--1 提取 list 集合类型属性注入-->
<util:list id="bookList"> //工具集合,其他类可以都可以引用
  <value>易筋经</value>
  <value>九阴真经</value>
  <value>九阳神功</value>
</util:list>
<!--2 提取 list 集合类型属性注入使用-->
<bean id="book" class="com.atguigu.spring5.collectiontype.Book">
<property name="list" ref="bookList"></property>
</bean>

2.给对象的属性赋值的三种方式

1.通过有参构造器

​ 使用 标签
​ name属性:构造器中参数的名字
​ value属性:表示设置的参数值
​ index属性:指定当前参数在构造器中的位置
​ type属性:指定当前参数的类型
结论:通过构造器给对象的属性赋值的时候,我们可以通过构造器的name、index(从0开始)、type属性可以唯一的确定调用任何一个构造器

<bean class="com.atguigu.bean.Book">
   <constructor-arg name="bid" value="109" index="1" type="java.lang.Integer"/>
   <constructor-arg name="bookName" value="斗破苍穹"/>
   <constructor-arg name="price" value="100.89"/>
</bean>
2.通过set方法

①直接使用property标签 ②通过p名称空间(对property标签的简化)

<!-- 2.1直接通过set方法给对象的属性赋值 -->
<bean id="book1" class="com.atguigu.bean.Book">
    <property name="bid" value="100"/>
    <property name="bookName" value="java从入门到放弃"/>
    <property name="price" value="30"/>
</bean>
<!-- 2.2(通过p名称空间)通过set方法给对象的属性赋值 需要先创建约束 xmlns:p="http://www.springframework.org/schema/p" -->
<bean id="book1" class="com.atguigu.bean.Book" p:bid="109" p:bookName="一剪没" p:price="32"></bean>
          <!--如果属性是自定义的引用数据类型使用p:bid-ref="..." p:bookName-ref="..."  p:price-ref="..."-->
3.通过注解的方式(见下面)

3.Spring的内置工厂 bean

1、Spring 有两种类型 bean,一种普通 bean,另外一种工厂 bean(FactoryBean)
2、普通 bean:在配置文件中定义 bean 类型就是返回类型
3、工厂 bean:在配置文件定义 bean 类型可以和返回类型不一样
第一步 创建类,让这个类作为工厂 bean,实现接口 FactoryBean
第二步 实现接口里面的方法,在实现的方法中定义返回的 bean 类型

public class MyBean implements FactoryBean<Course> {
    //定义返回 bean
    @Override
    public Course getObject() throws Exception {
        Course course = new Course();
        course.setCname("abc");
        return course;
    }
    @Override
    public Class<?> getObjectType() {
        return null;
    }
    @Override
    public boolean isSingleton() {
        return false;
    }
}

<bean id="myBean" class="com.atguigu.spring5.factorybean.MyBean" />

4.bean 作用域:scope 属性

影响bean对象的创建时机、是否单例

1、在 Spring 里面,默认情况下,设置创建 bean 实例 是单实例对象(singleton)
2、如何设置单实例还是多实例(scope 属性)
(1)在 spring 配置文件 bean 标签里面有属性(scope)用于设置单实例还是多实例
(2)scope 属性值
singleton:表示该类对象是单例的,是随着容器的创建而创建,
prototype:表示该类对象是多例的,获取的时候再创建,获取几次就创建几次

5.SPEL 表达式[了解]

SPEL:全称 Spring Expression Language:Spring 的表达式语言
SPEL 语法:#{}
支持:
字面量:true,1,”张三”
普通对象[普通对象属性,普通对象的普通方法]
类的普通属性、普通方法、静态属性、静态方法
各种各样的运算符:
①算术运算符:+、-、*、/、%、^
②字符串连接:+
③比较运算符:<、>、==、<=、>=、lt、gt、eq、le、ge
④逻辑运算符:and, or, not, |
⑤三目运算符:判断条件?判断结果为true时的取值:判断结果为false时的取值
⑥正则表达式:matches

<bean id="book" class="com.com.atguigu.bean.Book">
    <property name="name" value="为奴十二年"></property>
</bean>
<bean class="com.com.atguigu.bean.Student">
    <property name="sid" value="#{23}"></property>
    <property name="studentName" value="#{book.name}"></property>
    <property name="gender" value="#{12 > 10}"></property>
    <property name="book" value="#{book}"></property>
</bean>

6.基于注解的方式创建bean对象

第一步:导入 jar 包

image-20210716104738670

第 二 步 : 在 spring 的 配 置 文 件 中 配 置 一 个 <context:component-scan basePackage=”...”>

image-20210716104838613

第三步:在扫描包或者扫描包的子包下类上加@Component 注解

@Scope
标记在类上、方法上,使用和含义同标签的 scope 属性一样

7.xml 自动装配

1、什么是自动装配
(1)根据指定装配规则(属性名称或者属性类型),Spring 自动将匹配的属性值进行注入
2、演示自动装配过程
(1)根据属性名称自动注入
autowire 属性常用两个值:
byName 根据属性名称注入 ,注入值 bean 的 id 值和类属性名称一样
byType 根据属性类型注入

<bean id="emp" class="com.atguigu.spring5.autowire.Emp" autowire="byName">
<!--<property name="dept" ref="dept"></property>-->
</bean>
<bean id="dept" class="com.atguigu.spring5.autowire.Dept"></bean>

(2)根据属性类型自动注入

<bean id="emp" class="com.atguigu.spring5.autowire.Emp" autowire="byType">
    <!--<property name="dept" ref="dept"></property>-->
</bean>
<bean id="dept" class="com.atguigu.spring5.autowire.Dept"/>

8.加载 properties 文件

<!-- 导入Druid的properties配置文件-->
<context:property-placeholder location="druid_jdbc.properties"></context:property-placeholder>

<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" >
    <property name="url" value="${jdbc.jdbcUrl}"></property>
    <property name="driverClassName" value="${jdbc.driverClass}"></property>
    <property name="username" value="${jdbc.userName}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>

8.IOC 操作 Bean 管理(基于注解)

1、什么是注解

(1)注解是代码特殊标记,格式:@注解名称(属性名称=属性值, 属性名称=属性值..)
(2)使用注解,注解作用在类上面,方法上面,属性上面
(3)使用注解目的:简化 xml 配置

2、Spring 针对 Bean 管理中创建对象提供注解

(1)@Component: 标记在类上,表示当前类对象是容器中的一个普通组件,强调不要加在bean层类上
(2)@Service:标记在类上,表示当前类对象是容器中的一个service层组件
(3)@Controller:标记在类上,表示当前类对象是容器中的一个web层组件
(4)@Repository:标记在类上,表示当前类对象是容器中的一个dao层组件

上面四个注解功能是一样的,都可以用来创建 bean 实例

3、基于注解方式创建对象

    1. (在上面xml配置jar包基础上)引入依赖
image-20210716105831258
    1. 开启组件扫描(applicationContext.xml中配置)
<!--开启组件扫描//扫描的是内部全部子文件
    1 如果扫描多个包,多个包使用逗号隔开
    2 扫描包上层目录
-->
<context:component-scan base-package="com.atguigu"/>
    1. 创建类,在类上面添加创建对象注解
//在注解里面 value 属性值可以省略不写,
//默认值是类名称,首字母小写
//UserService -- userService
@Component(value = "userService") //<bean id="userService" class=".."/>
public class UserService {
    public void add() {
        System.out.println("service add.......");
    }
}
  • 4.开启组件扫描细节配置

    整合Spring和SpringMVC配置说明

    SpringMVC 主要就是来管理网站的跳转逻辑,所以在配置扫描的包时,使用 use-default-filters 属性,并设置为 false,即不使用默认的 Filter 进行扫描。

    use-default-filters 属性的默认值为 true,即使用默认的 Filter 进行包扫描,而默认的 Filter 对标有 @Service,@Controller和@Repository 的注解的类进行扫描。

    而我们需要 SpringMVC 控制网站的跳转逻辑,所以我们只希望 SpringMVC 的配置扫描 @Controllerce 注解标注的类,不希望它扫描其余注解标注的类,所以设置了 use-default-filters 为 false,并使用 context:include-filter 子标签设置其只扫描带有 @Controller 注解标注的类。
    而 Spring ,我们希望 Spring 扫描 @Controller 注解之外标注的类,这时建立在使用默认的 Filter 进行扫描的基础上,设置了 context:exclude-filter 标签,不扫描 @Controller 注解标注的类,所以不设置 use-default-filters 为 false

<!--示例 1
    use-default-filters="false" 表示现在不使用默认 filter,自己配置 filter
    context:include-filter ,自己设置扫描哪些内容(比如只扫描带...注解的类)
-->
<context:component-scan base-package="com.atguigu" use-default filters="false">
    <context:include-filter type="annotation" 
        expression="org.springframework.stereotype.Controller"/> //表示扫描Controller注解的类
</context:component-scan>

<!--示例 2
    下面配置表示扫描包所有内容
    context:exclude-filter: 表示设置哪些内容不进行扫描
-->
<context:component-scan base-package="com.atguigu">
    <context:exclude-filter type="annotation"
        expression="org.springframework.stereotype.Controller"/> //表示不扫描Controller注解的类
</context:component-scan>
  • 5.基于注解方式实现属性注入
    (1)@Autowired:根据属性类型进行自动装配(Spring中的)
    第一步 把 service 和 dao 对象创建,在 service 和 dao 类添加创建对象注解
    第二步 在 service 注入 dao 对象,在 service 类添加 dao 类型属性,在属性上面使用注解

    ​ @Autowired使用原理:当在一个属性上加了@Autowired之后,

    ​ 默认是现根据当前属性的类型去容器中找该类型的唯一bean对象,

    ​ 第一种情况:容器中没有该类型的唯一bean对象,报错

    ​ 第二种情况:容器中有该类型的唯一bean对象,将容器中的唯一bean对象赋值给该属性

    ​ 第三种情况:容器中有多个该类型的唯一bean对象,接着会根据当前属性名去容器中找 ,看看哪个bean对象的id值和当前属性名一致: 如果有,就进行赋值,如果没有就报错。

@Service
public class UserService {
    //定义 dao 类型属性
    //不需要添加 set 方法
    //添加注入属性注解
    @Autowired //@Autowired如果要使用byName,需要使用@Qualifier一起配合
    private UserDao userDao;
    public void add() {
        System.out.println("service add.......");
        userDao.add();
    }
}

​ (2)@Qualifier:根据名称进行注入

​ 这个@Qualifier 注解的使用,和上面@Autowired 一起使用(同一类型bean对象有多个时)

//定义 dao 类型属性
//不需要添加 set 方法
//添加注入属性注解
@Autowired //根据类型进行注入
@Qualifier(value = "userDaoImpl1") //根据名称进行注入,有此名的对象就能注入(即使没有@Autowired)
private UserDao userDao;

(3)@Resource:可以根据类型注入,可以根据名称注入(jdk中的)

//@Resource //@Resource默认byName自动装配,@Autowired默认按byType自动装配
@Resource(name = "userDaoImpl1") //@Resource如果指定了name,则用byName自动装配,如果指定了type,则用byType自动装配。
private UserDao userDao;

(4)@Value:注入普通类型属性//相当于property标签

@Value(value = "abc")
private String name;

3.Aop

1.Aop 简介[常见面试题]

AOP:全称:Aspect Oriented Programming:面向切面编程
OOP:全称:Object Oriented Programming:面向对象编程

软件工程有个基本的概念,叫做关注点分离
关注点分离[Separation of Concerns]: 不同的问题交给不同的部分去解决,每部分专注于解决自己的问题!

AOP(概念)
Aop 含义:
全称:Aspect Oriented Programming:面向切面编程
Aop 是在不改变原来 OOP 类代码的基础之上,对原来类的功能进行拓展。
Aop 和 OOP 不存在谁取代谁的关系,它们是相互促进,相互补充,
AOP 作用:解决了软件工程中的关注点分离问题。可以让系统变得高内聚、低耦合、便于项目后期的维护和拓展。
Aop 底层原理:动态代理

2.AOP(底层原理:动态代理)

Spring AOP默认依旧使用JDK动态代理

SpringBoot 1.x 默认依旧使用JDK动态代理

SpringBoot 2.x开始,为了解决使用JDK动态代理可能导致的类型转换异常而默认使用 CGLIB动态代理

在SpringBoot 2.x中,如果需要替换使用JDK动态代理可以通过配置项spring.aop.proxy-target-class=false来进行修改

image-20210716110955175

1.AOP 底层使用2种动态代理

​ 第一种 类有接口情况,使用 JDK 动态代理
​ 创建接口实现类代理对象,增强类的方法(代理对象是被代理类的兄弟类)
​ 第二种 类没有接口情况,使用 CGLIB 动态代理
​ 创建子类的代理对象,增强类的方法(代理对象是被代理类的子类)

2.静态代理和动态代理

代理模式
为另一个对象提供一个替身或占位符以控制这个对象的访问。
代理就是找个人帮你做事情,好比你要买房子,代理就相当于房地产中介,帮你去寻找合适的房源,我们需要把自己对房源的需求告诉房地产中介,中介在找房源的过程中还会和我们不断的沟通。

为什么要有代理模式?
为了解决直接操作目标对象给系统带来的不必要的复杂性。

静态代理

public interface ISale {
    //卖烧饼
    void saleShaoBing();
}

//目标对象 - Target
public class WuDa implements ISale {
    //目标方法 - 本质工作
    @Override
    public void saleShaoBing() {
        System.out.println("卖烧饼....");
    }
}

//代理对象 - proxy
public class WuDaProxy implements ISale {
    private ISale target = new WuDa();
    @Override
    public void saleShaoBing() {
        song();     //前置增强
        target.saleShaoBing();
        song();     //后置增强          //环绕增强      //抛异常增强
    }
    //额外操作 - Advice(增强)
    private void song(){
        System.out.println("送大麦茶...");
    }
}

//客户端测试
public class ProxyTest {
    @Test
    public void test01(){
        ISale isale = new WuDa();
        isale.saleShaoBing();
    }
    @Test
    public void test02(){
        ISale isale = new WuDaProxy();
        isale.saleShaoBing();
    }
}

静态代理在使用时必须事先知道真实对象的存在,并将其作为代理对象的内部属性;

一个真实对象对应一个代理对象,如果真实对象很多,那么代理对象的大量使用将是个灾难;

此外,如果事先不知道真实对象是谁, 该怎么使用代理呢? 下面的动态代理就解决了这个问题。

动态代理

实现方式
1.jdk代理:InvocationHandler Proxy
2.cglib代理: MethodInterceptor Enhancer

1.JDK动态代理详解

JDK 动态代理,使用 Proxy 类的 newProxyInstance() 方法创建代理对象

newProxyInstance() 返回值:代理类对象

image-20210716111100607

第一个参数:被代理类的类加载器(一般都是AppClassLoader)
第二个参数:被代理类实现的接口,支持多个接口
第三个参数:InvocationHandler接口的实现类对象,可以在此接口实现类中对被代理类进行拓展。

万能代理代码:(版本一 需要两个参数:1.目标类接口 2.目标类对象)
传入目标类对象即可生成代理对象,接口在此只充当作为返回值类型接收生成的代理类对象

public class MyInvocationHandler<T> implements InvocationHandler {
    private T target;
    
    //new对象时调用有参构造,将目标类对象传进来
    public MyInvocationHandler(T target){
        this.target = target;
    }
      /************************************************************
     newProxyInstance()参数:
       第一个参数:被代理类的类加载器
       第二个参数:被代理类实现的接口
       第三个参数:InvocationHandler接口的实现类对象,咱们可以在InvocationHandler接口的实现类中对被代理类进行拓展。
     newProxyInstance()返回值:表示代理类对象
    ************************************************************/
    public T proxy(){
        T o = (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
        return o;
    }

    //method:表示被代理类的目标方法,
    //args:当调用被代理类方法的时候传递的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前置增强。。。");
        //调用被代理类的目标方法
        Object result = method.invoke(target, args);
        System.out.println("后置增强");
        return result;
    }
}

测试:

public void test01(){
   // RealPayment realPayment = new RealPayment();
    WeiPayImpl weiPay = new WeiPayImpl();
      //传入了两个参数:1.WeiPay接口 2.WeiPayImpl目标类对象
    MyInvocationHandler<WeiPay> result = new MyInvocationHandler<WeiPay>(weiPay);
    WeiPay proxy = result.proxy();
    proxy.pay(32.8);
}

万能代理代码:(版本二 需要1个参数:目标类对象)

public class MyInvocationHandler2 implements InvocationHandler {
    private Object target;

    //new对象时调用有参构造,将目标类对象传进来
    public MyInvocationHandler2(Object target){
        this.target = target;
    }
    /************************************************************
     newProxyInstance()参数:
     第一个参数:被代理类的类加载器
     第二个参数:被代理类实现的接口
     第三个参数:InvocationHandler接口的实现类对象,咱们可以在InvocationHandler接口的实现类中对被代理类进行拓展。
     newProxyInstance()返回值:表示代理类对象
     ************************************************************/
    public Object proxy(){
        Object o = Proxy.newProxyInstance(target.getClass().getClassLoader(), 		target.getClass().getInterfaces(),this);
        return o;
    }

 //该方法集中处理动态代理类上的所有方法调用,
    // 第一个参数即是代理类实例,
    // 第二个参数是被调用的方法对象,
    // 第三个参数是调用参数,
    // 调用处理器根据这三个参数进行预处理或分派到委托类示例上反射执行
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("把钱从银行取出来...");
        //调用被代理类的目标方法
        Object result = method.invoke(target, args);
        System.out.println("把钱付给商家...");
        return result;
    }
}
public void test02(){
    // RealPayment realPayment = new RealPayment();
    WeiPayImpl weiPay = new WeiPayImpl();
    //传入了两个参数:1.WeiPay接口 2.WeiPayImpl目标类对象
    MyInvocationHandler2 result = new MyInvocationHandler2(weiPay);
    WeiPay proxy = (WeiPay)result.proxy();
    proxy.pay(32.8);
}

万能代理代码:(版本三 需要1个参数:目标类对象;创建代理类对象使用了2种方法)

class DynamicSubject implements InvocationHandler{
    //真实对象的引用
    private Object sub;
    public DynamicSubject(Object sub){
        this.sub = sub;
    }

    @Override
    //该方法集中处理动态代理类上的所有方法调用,第一个参数既是代理类实例,第二个参数是被调用的方法对象,第三个参数是调用参数,调用处理器根据这三个参数进行预处理或分派到委托类示例上反射执行
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //方法调用之前
        System.out.println("方法调用之前:买房之前需要交中介费" + method);
        //执行方法
        method.invoke(sub, args);
        //方法调用之后
        System.out.println("方法调用之后:买房之后的一些操作" + method);
        return null;

    }
}
/*********************************************************/
//接口:声明目标对象需要让代理对象帮忙做的事情
interface Subject {
    public void buyHouse();
}
/*********************************************************/
//创建目标对象(真实对象)
class RealSubject implements Subject {
    @Override
    public void buyHouse() {
        System.out.println("我想买一个房子");
    }
}
/*********************************************************/
//客户端测试
public class Proxytest {
    public static void main(String[] args) throws Throwable{
        //方法一:newProxyInstance 封装的步骤
        RealSubject realSubject = new RealSubject();
        Class clazz = realSubject.getClass();
        InvocationHandler handler = new DynamicSubject(realSubject);
        //Proxy提供了静态方法为指定类装载器、一组接口以及调用处理器生成动态代理实例
        Subject subject = (Subject) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), handler);

         //方法二:newProxyInstance 封装的步骤
        //通过为Proxy 类指定Classloader对象和一组Interface来创建动态代理类
       Class c = Proxy.getProxyClass(clazz.getClassLoader(),clazz.getInterfaces());
       //通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理类接口类型
       Constructor ct = c.getConstructor(new Class[]{InvocationHandler.class});
       //通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入
       Subject subject = (Subject)ct.newInstance(new Object[]{handler});
        subject.buyHouse();

    }
}

可以看出,JDK 动态代理使用反射实现的,反射中Proxy可以根据传入参数的不同(handler)在运行时产生不同的代理类,该代理类必须是实现了被代理类的接口,并且有个参数为InvocationHandler的构造函数,所以jdk动态代理的缺点是:只能代理接口定义的方法
为了解决这个问题,出现了动态代理的第二种实现方式—CGlib

2.Cglib动态代理详解

Cglib 是针对类来实现代理的,原理是对指定的业务类生成一个子类并覆盖其中业务方法实现代理,因为采用的是继承,所以不能对final修饰的类进行代理

//定义业务类,不是必须要实现接口
//创建目标对象(真实对象)
class RealSubject2 {
    public void buyHouse() {
        System.out.println("我想买一个房子");
    }
}
/*********************************************************/
/*
 * 通过实现MethodInterceptor方法代理接口,创建代理类
 */
class CglibProxy implements MethodInterceptor{
    // 创建加强器,用来创建动态代理类
    private Enhancer enhancer = new Enhancer();

    //传进去的clazz 为业务类对象,供代理方法中进行真正的业务方法调用
    public Object getProxy(Class clazz){
        //为加强器指定要代理的业务类(为此方法生成的代理类指定父类(即将业务类对象设置为代理类的父类))
        enhancer.setSuperclass(clazz);
        //设置回调:对于代理类上所有方法的调用,都会调用callback,而callback则需要实现intercept()方法进行拦截
        enhancer.setCallback(this);
        //创建动态代理类对象并返回
        return enhancer.create();
    }

    //实现回调方法
    @Override
    public Object intercept(Object arg0, Method method, Object[] args, MethodProxy proxy) throws Throwable {

        //方法调用之前
        System.out.println("方法调用之前:买房之前需要交中介费" + method);
        //执行方法,调用业务类(父类)中的方法
        Object res = proxy.invokeSuper(arg0, args);
        //方法调用之后
        System.out.println("方法调用之后:买房之后的一些操作" + method);
        return res;

    }

}
/*********************************************************/
//客户端测试
public class Proxytest2 {
    public static void main(String[] args) {
        CglibProxy proxy = new CglibProxy();
        RealSubject2 rs  = (RealSubject2) proxy.getProxy(RealSubject2.class);
        rs.buyHouse();
    }
}

3.AOP术语

​ 通知[Advice]: 拓展(增强)的功能:本质是一个方法(通知方法)

​ 切面[Aspect]: 通知所在的类就是切面,本质是一个类,可以声明多个通知方法

​ 连接点[JoinPoint]:通知和目标方法的交点(spring仅支持方法级别的连接点)

​ 切入点[PointCut]: 用来确定对谁进行拓展的,本质是一个表达式(被增强的方法)(所有切点都是都是连接点,连接点不一定是切点)

​ 目标对象[Target]: 被增强(拓展)的类对象

​ 织入[Weaving]: 将通知应用到目标方法的过程

​ 代理[Proxy]: 向目标对象应用通知之后创建的代理对象

image-20210716111350722

4.Aop 具体实现步骤

第一步:创建动态 web 工程,导入 jar 包

image-20210716111449772

第二步:需要将被拓展的类和拓展的类加入到容器中:扫描包+注解
第三步:在 spring 配置文件中开启基于注解的切面支持及在拓展的类上加@Aspect 注解
applicationContext.xml中:

<context:component-scan base-package="com.atguigu"/>
<!--开启基于注解的切面支持-->
<aop:aspectj-autoproxy/>
@Component
@Aspect //该注解标记的类表示当前类是一个切面类
public class LogAspect {
    @Before(value = "execution(public int com.atguigu.aop.CaculatorImpl.add(int,int))")
    public void beforeLog(){
        System.out.println("在目标方法执行之前打印........");
    }
    @After(value = "execution(public int com.atguigu.aop.CaculatorImpl.add(int,int))")
    public void afterLog(){
        System.out.println("在目标方法执行之后打印........");
    }
}

第四步:在拓展的类的方法上指定切入点表达式

5.切面中的五种通知

1.前置、后置、返回、异常通知

前置通知[@Before]:在目标方法执行之前执行的功能.
后置通知[@After]:无论目标方法是否执行成功,在目标方法执行之后,后置通知都会执行(又称最终通知,类似finally)
返回通知[@AfterReturning]:目标方法执行成功之后,返回通知才会执行。
异常通知[@AfterThrowing]:目标方法执行出现异常的时候,异常通知才会执行。
环绕通知[@Around]:以一抵四。

@Component
@Aspect //该注解标记的类表示当前类是一个切面类
public class LogAspect {
    //前置通知
    @Before(value = "execution(public int com.atguigu.aop.CaculatorImpl.*(int,int))")
    public void beforeLog(){
        System.out.println("日志切面前置通知");
    }
    //后置通知
    @After(value = "execution(public int com.atguigu.aop.CaculatorImpl.*(int,int))")
    public void afterLog(){
        System.out.println("日志切面后置通知");
    }
    //返回通知
    @AfterReturning(value = "execution(public int com.atguigu.aop.CaculatorImpl.*(int,int))")
    public void afterReturningLog(){
        System.out.println("日志切面 返回通知");
    }
    //异常通知
    @AfterThrowing(value = "execution(public int com.atguigu.aop.CaculatorImpl.*(int,int))")
    public void afterThrowingLog(){
        System.out.println("日志切面 异常通知");
    }
}


2.使用环绕通知实现事务切面

环绕通知特征

  • 1.必须返回Object
  • 2.必须要有ProceedingJoinPoint参数
  • 3.必须抛出Throwable类型的异常
  • 4.必须手动执行目标方法:joinPoint.proceed()
image-20210716112212585
@Component
@Aspect
public class TransactionAspect {
    /************************************************************
环绕通知:
1.要求环绕通知标记的方法返回值类型必须为Object类型
2.要求将目标方法的返回值返回。
************************************************************/
    @Around(value = "com.atguigu.aop.LogAspect.myPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String name = joinPoint.getSignature().getName();//获取目标方法名
        Object[] args = joinPoint.getArgs();//获取传递给目标方法的参数值信息
        Object proceed = null;
        try {
            try{
                //1.前置通知
                System.out.println("事务切面 前置通知");
                //放行请求:执行目标方法
                proceed = joinPoint.proceed(args);
            }finally {
                System.out.println("事务切面 后置通知");
            }
            System.out.println("事务切面 返回通知");
        } catch (Throwable ex){
            ex.printStackTrace();
            System.out.println("事务切面  异常通知,目标方法的异常信息为:"+ex.getMessage());//获取目标方法的异常信息
            throw new RuntimeException(ex.getMessage());//抛出异常给外面,不然外面不会知道
        }
        return proceed;
    }
谷粒商城AOP封装注解
@Target({ElementType.METHOD})  //作用在方法上的注解
@Retention(RetentionPolicy.RUNTIME)  //运行时注解
//@Inherited //是否可继承
@Documented  //是否加入生成的文档中
public @interface GmallCache {

    /**
     * 缓存的前缀,默认:gmall:
     * @return
     */
    String prefix() default "gmall:";

    /**
     * 缓存的过期时间。默认30min
     * @return
     */
    int timeout() default 30;

    /**
     * 为了防止缓存雪崩,给缓存时间添加随机值。这里可以指定随机值范围
     * 默认10min
     * @return
     */
    int random() default 10;

    /**
     * 为了防止缓存击穿,添加分布式锁。这里可以指定分布式锁的前缀
     * 默认lock:
     * @return
     */
    String lock() default "lock:";
}
@Aspect //该注解标记的类表示当前类是一个切面类
@Component
public class GmallCacheAspect { 
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    @SuppressWarnings("all")
    private RBloomFilter bloomFilter;

    /**
     * 切面表达式:@annotation(com.atguigu.gmall.index.annotation.GmallCache
     * 只针对 @GmallCache 注解添加切面
     * <p>
     * 1.必须返回Object
     * 2.必须要有ProceedingJoinPoint参数
     * 3.必须抛出Throwable类型的异常
     * 4.必须手动执行目标方法:joinPoint.proceed() 并将目标方法的返回值返回
     * <p>
     * 通知方法:
     * 1.获取目标方法参数:joinPoint.getArgs()
     * 2.获取目标方法签名:
     */
    @Around("@annotation(com.atguigu.gmall.index.annotation.GmallCache)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取方法对象
        Method method = signature.getMethod();
        // 获取方法对象上的指定注解
        GmallCache gmallCache = method.getAnnotation(GmallCache.class);
        // 获取注解中的前缀
        String prefix = gmallCache.prefix();

        // 方法的形参列表,这里以逗号分割,组装程字符串
        String args = StringUtils.join(joinPoint.getArgs(), ",");
        String key = prefix + args;

        // 为了防止缓存穿透,使用布隆过滤器
        if (!bloomFilter.contains(key)) {
            return null;
        }

        // 1.先查询缓存,命中则直接返回
        String json = this.redisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(json)){
            return JSON.parseObject(json, signature.getReturnType());
        }

        // 2.防止缓存击穿,添加分布式锁
        String lock_prefix = gmallCache.lock();
        RLock lock = this.redissonClient.getLock(lock_prefix + args);
        lock.lock();

        try {
            // 3.再查缓存,命中则直接返回
            String json2 = this.redisTemplate.opsForValue().get(prefix + args);
            if (StringUtils.isNotBlank(json2)){
                return JSON.parseObject(json2, signature.getReturnType());
            }

            // 4.执行目标方法
            Object result = joinPoint.proceed(joinPoint.getArgs());

            // 5.放入缓存,并释放分布式锁
            int timeout = gmallCache.timeout() + new Random().nextInt(gmallCache.random());
            this.redisTemplate.opsForValue().set(key, JSON.toJSONString(result), timeout, TimeUnit.MINUTES);

            return result;
        } finally {
            lock.unlock();
        }
    }
}

3.多切面情况下,切面执行顺序

内层切面嵌套在外层切面中 proceed = joinPoint.proceed(); 的位置
多切面情况下,切面的执行顺序是由标记在切面类上的@Order 决定的,@Order 注解的
value 属性值越小,切面的优先级越高。(默认根据包名+类名的自然排序决定)

6.通知的底层结构

(4.x版本和5.x版本不一样,5.x版本更合理)

image-20210716111725777

7.切入点表达式

详见5.1

作用:用来确定对谁进行拓展的。
语法:execution([权限修饰符] [返回值类型] [全类名] 方法名)
最简洁[最模糊]:
execution(* .(..))
最复杂[最精确]:
execution(public int com.atguigu.aop.CaculatorImpl.add(int,int))
切入点表达式重用:

@Component
@Aspect //该注解标记的类表示当前类是一个切面类
public class LogAspect {
    @Pointcut(value = "execution(public int com.atguigu.aop.CaculatorImpl.*(..))")
    public void myPointCut(){}
    //前置通知
    @Before(value = "myPointCut()")//可以直接引用本类或者外部类定义的Pointcut所在的方法
    public void beforeLog(){
        System.out.println("日志切面 前置通知");
    }
}

切入点表达式支持 与、或、非操作:
在 AspectJ 中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
表达式 execution (* .add(int,..)) || execution( .sub(int,..))
含义:任意类中第一个参数为 int 类型的 add 方法或 sub 方法

从通知中获取目标方法信息

image-20210716111922329

前置通知:获取目标方法名和传递给目标方法的参数值信息

image-20210716112001311

返回通知:获取目标方法返回结果

image-20210716112044724

异常通知:获取目标方法的异常信息

image-20210716112135215

8.基于 xml 配置的 AOP

<bean id="caculator" class="com.atguigu.aop.CaculatorImpl"></bean>
<bean id="logAspect" class="com.atguigu.aop.LogAspect"></bean>

<aop:config>
    <aop:pointcut id="myPointCut" expression="execution(public int com.atguigu.aop.CaculatorImpl.*(int,int)))"/>
    <aop:aspect ref="logAspect">
        <aop:before method="beforeLog" pointcut-ref="myPointCut"></aop:before>
        <aop:after-returning method="afterReturningLog" pointcut-ref="myPointCut" returning="result"></aop:after-returning>
        <aop:after-throwing method="afterThrowingLog" pointcut-ref="myPointCut" throwing="ex"></aop:after-throwing>
        <aop:after method="afterLog" pointcut-ref="myPointCut"></aop:after>
    </aop:aspect>
</aop:config>

3.JdbcTemplate

Spring 的 JdbcTemplate 实际上就是 Spring 框架对原生 JDBC 的简单封装,类似于
Javaweb 部分学过 DbUtils 工具类,主要是对数据表的数据进行增删改查操作。

1.具体使用步骤

第一步:创建动态 web 工程,导入 jar 包

image-20210716112532714

第二步:在 spring 的配置文件配置 JdbcTemplate

<!-- 1.加载properties文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 2.配置数据源信息-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
    <property name="username" value="${jdbc.userName}"/>
    <property name="password" value="${jdbc.password}"/>
    <property name="driverClassName" value="${jdbc.driverClass}"/>
    <property name="url" value="${jdbc.jdbcUrl}"/>
</bean>
<!-- 3.配置JdbcTemplate-->
<bean class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>

第三步:在 dao 层使用 JdbcTemplate
@Autowired//直接在dao层类中注入JdbcTemplate
private JdbcTemplate jdbcTemplate;

2.JdbcTemplate的使用

​ 增删改操作:update()方法
​ 批量增删改:batchUpdate()方法
​ 查询操作:
​ 1.查询一个pojo对象:queryForObject()
​ 2.查询一个单值:queryForObject()
​ 3.查询一个对象列表:query()方法

//要求表的列名要和RowMapper中泛型指定bean的属性名保持一致。如果不一致,通过起别名的方式让它一致。
public Employee getEmployeeByEid(Integer eid){
    String sql="select emp_id eid,emp_name ename,salary from employee where emp_id = ?";
    RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<Employee>(Employee.class);
    Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, eid);
    return employee;
}

public List<Employee> getEmployeesByEid(Integer eid){
    String sql="select emp_id eid,emp_name ename,salary from employee where emp_id > ?";
    RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<Employee>(Employee.class);
    List<Employee> list = jdbcTemplate.query(sql, rowMapper, eid);
    return list;
}

public double getMaxSalary(){
    String sql="select max(salary) from employee";
    Double maxSalary = jdbcTemplate.queryForObject(sql, Double.class);
    return maxSalary;
}

public void batchInsert(){
    String sql="update employee set salary =? where emp_id = ?";
    List<Object[]> list = new ArrayList<Object[]>();
    list.add(new Object[]{1});
    list.add(new Object[]{2});
    list.add(new Object[]{"小吕智",12000});
    jdbcTemplate.batchUpdate(sql,list);
}
public void insert(Employee employee){
    String sql="insert into employee values(null,?,?)";
    jdbcTemplate.update(sql,employee.getEname(),employee.getSalary());
}
public void deleteByEid(Integer eid){
    String sql="delete from employee where emp_id = ?";
    jdbcTemplate.update(sql,eid);
}


public void update(Employee employee){
    String sql="update employee set salary =? where emp_id = ?";
    jdbcTemplate.update(sql,employee.getSalary(),employee.getEid());
}

3.事务

事务的概念:事务是逻辑上一组操作,组成这组操作各个逻辑单元,要么一起成功,要么一起失败。

事务的使用场景:当在一个功能单元[方法]中涉及到对数据库的多次增删改操作,要求这
多次增删改操作要么同时成功,要么同时失败,这时候才会使用事务。

声明式事务在原生Spring中实现

第一步:导入 jar 包

image-20210716113017507

第二步:在 spring 的配置文件中配置数据源事务管理器,开启基于注解的事务支持

<!--1.配置扫描包 -->
<context:component-scan base-package="com.atguigu" ></context:component-scan>

<!-- 2.导入Druid的properies配置文件-->
<context:property-placeholder location="druid_jdbc.properties"></context:property-placeholder>

<!-- 3.配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" >
    <property name="url" value="${jdbc.jdbcUrl}"></property>
    <property name="driverClassName" value="${jdbc.driverClass}"></property>
    <property name="username" value="${jdbc.userName}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>

<!-- 4.配置JdbcTemplate -->
<bean  id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" >
    <property name="dataSource" ref="dataSource"/>
</bean>

<!-- 5.配置数据源事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 6.开启基于注解的事务支持 -->
<tx:annotation-driven transaction-manager="transactionManager"/>

第三步:在 service 层的方法上加@Transactional 注解

@Transactional失效原因和解决方法

​ 1.@Transactional 注解应该只被应用到 public 方法上

​ 如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常

​ 原因:AOP动态代理无法使用动态代理生产为对应的方法配置的事务设置

​ 2.类内部方法调用本类内部的其他加@Transactional注解的方法并不会引起事务行为

​ 原因:只有来自外部类的方法调用才会被AOP动态代理捕获,生产对应类的代理类对象并对方法配置的事务设置

事务的四大特性(ACID)

区别下面5大属性

  1. 原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。

  2. 一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。

  3. 隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰

  4. 持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。

声明式事务的五大属性

区别于上面4大特性

传播机制[propagation]

(Spring特有的,不是数据库的属性)

​ propagation 常用取值:Required、Requires_new、supports
​ Required[默认值]:表示外层方法 B 有事务,内层方法 A 就使用外层方法 B 的事务。
​ 表示外层方法 B 没有事务,内层方法 A 就使用自己的事务。
​ Requires_new:表示无论外层方法 B 有没有事务,内层方法 A 都是用自己的事务。
​ supports: 表示外层方法 B 有事务,内层方法 A 就使用外层方法 B 的事务。
​ 表示外层方法 B 没有事务,内层方法 A 就不用事务

七种传播行为:(分3组)

REQUIRED:支持a事务,如果a事务不存在则b新建事务
SUPPORTS:支持a事务,如果a事务不存在则b不使用事务
MANDATORY:支持a事务,如果a事务不存在则b抛出异常

REQUIRES_NEW:挂起a事务,b创建新的事务运行
NOT_SUPPORTED:挂起a事务,b以非事务方式运行
NEVER:b以非事务方式运行,如果a有事务,则抛出异常

NESTED:嵌套事务,基于jdbc3.0技术的savePoint,可以设置保存点,能回滚到指定的保存点

class A {
	@Transactional
	void a(){
		b();
		...
		// 异常
	}
}

class B{
	@Transactional(propagation=)
	void b(){	
	}
}
  • 依赖于JDBC3.0提供的SavePoint技术

  • 删除用户 删除订单。在删除订单后,设置savePoint,执行删除用户。删除订单和删除用户在同一事务中,删除用户失败,事务回滚savePoint,由用户控制视图提交还是回滚

这七种事务传播机制最常用的就两种:

REQUIRED:一个事务,要么成功,要么失败

REQUIRES_NEW:两个不同事务,彼此之间没有关系。一个事务失败了不影响另一个事务

隔离级别[isolation]

​ 隔离级别原理:对数据加锁来实现

​ read-committed就是加行锁,当前操作的行被锁在,其他操作不能修改当前行进行update,所有不会产生脏读

​ repeatable-read就是加表锁,当前操作的表被锁在,其他操作不能对当前表进行insert和delete,所以不会产生幻读

并发问题(通过设置隔离级别来解决)

事务并发引起一些读的问题:

  • 脏读 一个事务可以读取另一个事务未提交的数据

  • 不可重复读 一个事务可以读取另一个事务已提交的数据 单条记录前后不匹配

  • 虚读(幻读) 一个事务可以读取另一个事务已提交的数据 读取的数据前后多了点或者少了点

并发写:使用mysql默认的锁机制(独占锁)

解决读问题:设置事务隔离级别

  • read uncommitted(0)

  • read committed(2)

  • repeatable read(4)

  • Serializable(8)

隔离级别越高,性能越低。

针对并发访问的(脏读、不可重复读、幻读)

​ oracle 默认的隔离级别为:read_commited:读已提交(加行锁)

​ mysql 默认的隔离级别为:repeatable-read:可重复读(加表锁)

​ 所以oracle 执行性能要高于mysql

image-20210902002944941

回滚属性[rollbackFor]

​ 声明式事务默认只会在遇到不受检异常(运行时异常)的时候才会回滚!

​ 所有的不受检(运行时)异常都会回滚:例如 ArithmeticException OOM

​ 所有的受检(编译时)异常都不会回滚:例如 FileNotFoundExcption

​ 注意:Spring 在底层把 SQLException 已经转换为 RuntimeException
​ Spring 框架并没有将 IOException 转换为 RuntimeException

​ 自定义回滚策略:

​ rollbackFor=”异常类.class”

​ rollbackForClassName=”异常类全类名”

​ noRollbackFor=”异常类.class”

​ noRollbackForClassName=”异常类全类名”

image-20210716113244118

超时属性[timeout] 单位是秒

事务执行过程中,势必会占用着数据库资源,如果一个事务执行的时间过长,就会影响其他事务的执行,
所以为了防止一个事务长时间的占用着数据库资源,一般要给事务设置一个超时属性 timeout。
一旦事务在指定的时间内还没有完成这个操作,可以让事务回滚,释放数据库资源。
注意:超时时间是指多次对数据库进行的操作之间的总时间。

只读属性[readonly]

只读属性是为了加快查询效率而设置的,如果设置了 readonly=true,
就表示当前方法内部只涉及到对数据库的读取操作,而没有增删改操作,数据库底层会采用共享锁。

声明式事务(基于 xml 配置)

<!-- 配置数据源事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="druidDataSource"></property>
</bean>

<aop:config>
     <!-- 指定切入点表达式 -->
    <aop:pointcut id="myPointCut" expression="execution(* com.com.atguigu.service.BookService.*(..))"/>
    <!-- 2. 将切入点表达式和事务联系起来-->
    <aop:advisor advice-ref="dsada" pointcut-ref="myPointCut"></aop:advisor>
</aop:config>

<!-- 配置事务 //对哪些方法执行事务-->
<tx:advice id="dsada" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="buyBook"/>
        <tx:method name="get*" read-only="true"/>
        <tx:method name="find*"  read-only="true"/>
        <tx:method name="query*"  read-only="true"/>
        <tx:method name="update*" ></tx:method>
        <tx:method name="insert*" ></tx:method>
        <tx:method name="delete*" ></tx:method>
    </tx:attributes>
</tx:advice>

4.Spring 测试模块

具体使用步骤

第一步:导入spring-test.jar
第二步:在测试类上J加注解
@RunWith(SpringJunit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
//相当于new ClassPathXmlApplicationContext("applicationContext.xml");
public class BookServiceTest {

    @Autowired//直接注入BookService对象就可以使用,因为类上的注解获取到了容器
    private BookService bookService;
 
   @Test
    public void test01() {
        //ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        //BookService bookService = (BookService) context.getBean("bookService");
         bookService.buyBook("ISBN-001", "Tom");
    }
}

5.纯注解创建Bean容器,并注入依赖

​ 纯注解的面向切面编程、事务管理

@Configuration 标记在类上,表示当前类是一个配置类[容器类],用来取代 spring 的配置文件的。
@Bean 在容器类中配置bean对象
@ComponentScan 开启基于注解的扫描,代替applicationContext.xml文件中context:component-scan。。
@Import 引入其他的容器类(此外部容器里配的@Bean会被加载到容器中,此外部容器可以不加@Configuration注解)
@ImportResource 导入其它配置文件信息,相当于
@EnableAspectJAutoProxy 开启基于注解的切面支持 相当于aop:aspectj-autoproxy (然后再切面类上加@Aspect即可)
@EnableTransactionManagement 开启基于注解的事务支持 相当于tx:annotation-driven/

@Configuration //加了@Configuration的类就是配置类[容器类],相当于applicationContext.xml,主要作用创建bean对象
@ComponentScan(value = {"com.atguigu.service","com.atguigu.dao"})//开启基于注解的扫描,代替applicationContext.xml文件中<context:component-scan。。>
@Import(RandomConfig.class)  引入其他的容器类(此外部容器里配的@Bean会被加载到容器中,此外部容器可以不加@Configuration注解)
@ImportResource("classpath:applicationContext.xml")
@EnableAspectJAutoProxy  //相当于<aop:aspectj-autoproxy>  开启基于注解的切面支持
@EnableTransactionManagement//相当于<tx:annotation-driven/>  开启基于注解的事务支持
public class GlobalConfig {
     @Bean(name = "sds")//相当于<bean id="" class="">:方法返回值相当于class属性,方法名就相当于id值
    public Book getBook(){
        Book book = new Book();
        book.setBookName("干法");
        book.setPrice(33.3);
        book.setIsbn("ISBN-001");
        return book;
    }


    @Bean
    public DataSource druidDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/tx?rewriteBatchedStatements=true");
        return dataSource;
    }

     @Bean   //配置druid数据源
    public DataSource druidDataSource2(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/tx?rewriteBatchedStatements=true");
        return dataSource;
    }


    @Bean     //配置JdbcTemplate 来进行DAO操作                   //此处dataSource会优先到容器中寻找来应用,来注入依赖
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
    
    @Bean                                //此处dataSource指定了到容器中寻找id为druidDataSource2的DataSource类对象
    public JdbcTemplate jdbcTemplate(@Qualifier(value = "druidDataSource2") DataSource druidDataSource1){
    JdbcTemplate jdbcTemplate = new JdbcTemplate();
    jdbcTemplate.setDataSource(druidDataSource1);
    return jdbcTemplate;
    }




    @Bean   //配置事务源管理器  @EnableTransactionManagement注解已经开启了基于注解的事务支持,所以在想要加事务的service操作上加@Transactional注解就行了
    public DataSourceTransactionManager getDataSourceTransactionManager(@Qualifier(value = "druidDataSource2") DataSource druidDataSource1){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(druidDataSource1);
        return dataSourceTransactionManager;
    }


    //aop:导入xml+注解:纯xml、纯注解


}
//上面的容器类加了@Import(RandomConfig.class)来引用此容器类(此外部容器可以不加@Configuration注解)
public class RandomConfig {
    @Bean(name = "sds")//相当于<bean id="" class="">:方法返回值相当于class属性,方法名就相当于id值
    public Book getBook(){
        Book book = new Book();
        book.setBookName("干法");
        book.setPrice(33.3);
        book.setIsbn("ISBN-001");
        return book;
    }

    @Bean
    public DataSource druidDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/tx?rewriteBatchedStatements=true");
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
}
@Test
public void test01(){
    ApplicationContext context = new AnnotationConfigApplicationContext(GlobalConfig.class);
    Book bean = (Book)context.getBean("book");
    System.out.println(bean);
}

6.Spring底层DispacherServlet原理

让 tomcat 管理 spring 的配置文件或者配置类

1. 让 tomcat 加载 spring 的配置文件|配置类生成上下文对象
2. 放在 application 域
  方式一:(通过xml配置)
//1.此listener相当于书城项目写的ContextConfigLocationListener,
//2.在Tomcat启动时可以读取下面的初始化参数contextConfigLocation,把他的值即applicationContext放到application作用域中
//3.在用户第一次给DispatcherServlet发请求时,tomcat才会去(执行DispatcherServlet执行init初始化方法)创建单例的实例
//(Spring底层的DispatcherServlet在Tomcat启动时就会直接创建,而不用等收到客户请求才创建)
//4.然后从application保存作用域获取之前Listener中存放进去的数据,其实就是获取配置文件名applicationContext.xml
//5.根据这个配置文件去(通过beanFactory)创建指定的一个一个的Bean对象(DOM解析xml配置文件)
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>

书城项目参考代码:

@WebListener
public class ContextConfigLocationListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        //获取ServletContext对象(获取Servlet上下文)
        ServletContext application = servletContextEvent.getServletContext();
        //从上下文中获取初始化参数
        String contextConfigLocation = application.getInitParameter("contextConfigLocation");
        //将读取到的contextConfigLocation的值保存在application作用域,这样在其他地方就可以从application中获取
        application.setAttribute("contextConfigLocation",contextConfigLocation);
    }
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
    }
}
@WebServlet("*.action")
public class DispatcherServlet extends HttpServlet {
    private BeanFactory beanFactory ;
    @Override
    public void init(ServletConfig config) throws ServletException {
        //获取ServletContext上下文对象(获取application)
        ServletContext application = config.getServletContext();
        //从application保存作用域获取之前Listener中存放进去的数据,其实就是获取配置文件名applicationContext.xml
        String contextConfigLocation = (String)application.getAttribute("contextConfigLocation");
        //根据这个配置文件去创建指定的一个一个的Bean对象
        beanFactory = new ClassPathXmlApplicationContext(contextConfigLocation);
    }
public class ClassPathXmlApplicationContext implements BeanFactory {

    private String contextConfigLocation ;

    //创建多个Bean对象
    //组装Bean之间的依赖关系
    Map<String,Object> beanMap = new HashMap<>();
    
    //有参构造,配置文件applicationContext.xml会作为参数传过来然后创建所有的bean对象,并注入依赖
    public ClassPathXmlApplicationContext(String contextConfigLocation){
        //XML 文档对象模型定义访问和操作XML文档的标准方法。
        //DOM 将 XML 文档作为一个树形结构,而树叶被定义为节点。
        //DOM技术解析XML文档,DOM解析
        //1.创建DocumentBuilderFactory
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        try {
            //2.创建DocumentBuilder
            DocumentBuilder builder = factory.newDocumentBuilder();

            //3.创建Document对象
            InputStream is = this.class.getClassLoader().getResourceAsStream(contextConfigLocation) ;
            Document document = builder.parse(is);

            //步骤一:创建所有的bean对象
            //获取所有的bean标签
            NodeList beanNodeList = document.getElementsByTagName("bean");
            for(int i = 0 ; i<beanNodeList.getLength() ; i++){
                Node beanNode = beanNodeList.item(i);
                //得到一个bean标签
                Element beanElement = (Element)beanNode ;
                //获取id和className两个属性
                String id = beanElement.getAttribute("id");
                String className = beanElement.getAttribute("class");

                //根据全类名创建一个实例对象
                Object instance = createInstance(className);

                //将这个bean对象存到beanMap容器中
                beanMap.put(id,instance);
            }

            //步骤二:完成依赖关系的注入
            for(int i = 0 ; i<beanNodeList.getLength() ; i++) {
                Node beanNode = beanNodeList.item(i);
                //得到一个bean标签
                Element beanElement = (Element) beanNode;
                //获取id和className两个属性
                String id = beanElement.getAttribute("id");
                Object instance = beanMap.get(id);

                //获取bean标签内部的所有子节点
                NodeList childNodeList = beanElement.getChildNodes();
                for(int j = 0 ;j <childNodeList.getLength() ;j++){
                    Node beanChildNode = childNodeList.item(j);
                    //排除掉空白节点,只操作property子节点
                    if(beanChildNode.getNodeType() == Node.ELEMENT_NODE && "property".equals(beanChildNode.getNodeName())){
                        Element propertyElement = (Element)beanChildNode ;
                        String name = propertyElement.getAttribute("name");
                        String ref = propertyElement.getAttribute("ref");

                        Object refObj = beanMap.get(ref);

                        //依赖关系注入
                        setProperty(instance,name,refObj);
                    }
                }
            }
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void setProperty(Object instance , String property , Object value){
        //获取Class
        Class clazz = instance.getClass();
        try {
            //获取property对应的属性
            Field field = clazz.getDeclaredField(property);
            //防止是private,进行暴力访问
            field.setAccessible(true);
            //给这个property对应的属性赋值
            field.set(instance,value);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private Object createInstance(String fullName){
        try {
            return Class.forName(fullName).newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null ;
    }

    @Override
    public Object getBean(String id) {
        return beanMap.get(id);
    }
}

方式二:(通过注解-纯注解)//可以在Spring官网上DispatcherServlet里找到这段代码

public class MyWebApplicationInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // Load Spring web application configuration 加载Spring web应用程序配置
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(GlobalConfig.class); //GlobalConfig就是上面自己创建的加了@Configuration的类就是配置类[容器类]
        context.refresh(); //Spring4.x版本才有,5.x版本取消了refresh
    }
}

7.Bean对象的生命周期

Bean对象的生命周期(实际是7步,下面有补全)(了解即可,不重要)

(1)通过构造器创建 bean 实例(无参数构造)
(2)为 bean 的属性设置值和对其他 bean 引用(调用 set 方法)
(3)调用 bean 的初始化的方法(需要进行配置初始化的方法)
(4)bean 可以使用了(对象获取到了)
(5)当容器关闭时候,调用 bean 的销毁的方法(需要进行配置销毁的方法)

public class Orders {
    //无参数构造
    public Orders() {
        System.out.println("第一步 执行无参数构造创建 bean 实例");
    }

    private String oname;
    public void setOname(String oname) {
        this.oname = oname;
        System.out.println("第二步 调用 set 方法设置属性值");
    }

    //创建执行的初始化的方法
    public void initMethod() {
        System.out.println("第三步 执行初始化的方法");
    }

    //创建执行的销毁的方法
    public void destroyMethod() {
        System.out.println("第五步 执行销毁的方法");
    }
}

<bean id="orders" class="com.atguigu.spring5.bean.Orders" initmethod="initMethod" destroy-method="destroyMethod">
<property name="oname" value="手机"></property>
</bean>

控制台打印效果

image-20210716114046737

bean 的后置处理器,bean 生命周期有七步
(1)通过构造器创建 bean 实例(无参数构造)
(2)为 bean 的属性设置值和对其他 bean 引用(调用 set 方法)
3)把 bean 实例传递 bean 后置处理器的方法 postProcessBeforeInitialization
(4)调用 bean 的初始化的方法(需要进行配置初始化的方法)
(5)把 bean 实例传递 bean 后置处理器的方法 postProcessAfterInitialization
(6)bean 可以使用了(对象获取到了)
(7)当容器关闭时候,调用 bean 的销毁的方法(需要进行配置销毁的方法)

演示添加后置处理器效果
(1)创建类,实现接口 BeanPostProcessor,创建后置处理器
    public class MyBeanPost implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
        throws BeansException {
        System.out.println("在初始化之前执行的方法");
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
        throws BeansException {
        System.out.println("在初始化之后执行的方法");
        return bean;
    }
}

控制台打印效果

image-20210716114211693

posted on 2022-03-08 21:45  freyhe  阅读(55)  评论(0编辑  收藏  举报