Spring(三)

·  阅读 39

Spring

spring技术发展的四个阶段

第一阶段:spring-core:spring-core、spring security,spring data;

第二阶段:spring-boot:快速开发,提升程序的开发效率,使得程序可以可用变得好用;

第三阶段:spring-cloud微服务。推动微服务架构的落地,让不具备开发微服务的小型互联网公司也能享受到开箱即用的微服务解决方案。

第四阶段:spring cloud dataflow。基于微服务的分布式流处理和批处理数据通道,目标是简化大数据应用的开发。

Spring关键点

  1. spring可以说是Java世界最成功的框架,在企业级应用中,大部分的企业架构都基于Spring框架。
  2. Spring是开源的。
  3. Spring的出现是因为Sun EJB的失败。EJB是重量级技术,功能强大,但是配置繁杂,占用资源多,对EJB容器有依赖(侵入性),测试不方便,运行缓慢。而Spring是轻量级技术,保持技术强大的同时,克服了EJB的种种缺点。
  4. 2004年3月,Spring1.0发布。2017年9月,Spring5.0发布,基于JDK8
  5. Spring最核心的理念是IoC(控制反转)和AOP(面向切面编程)。基础是IoC,AOP最典型的应用当属数据库事务的处理。
  6. Spring避免了重复造轮子,而是提供更好的整合模板来整合这些技术,开发更简单。
  7. spring的特别的优点有:非侵入性或者低侵入性。使用POJO开发,不需要继承Spring API,通过配置扩展POJO功能,通过依赖注入、AOP、面向接口编程等,降低耦合性。即使Java应用离开了,Spring仍然可以运行。
  8. Spring已经从Spring一个产品发展成为一个家族,除了Spring之外,还有Spring MVC,Spring Security、SpringData、SpringBoot和SpringCloud等,但他们的基础都是Spring的IOC和AOP。网址:spring.io/projects

Spring优点

  • 非侵入性
  • IoC
  • AOP
  • 容器
  • 组件化
  • 一站式
  • 声明式

Spring五大模块

Core Container——核心容器,在Spring环境下使用任何功能都必须基于IoC容器。

AOP&Aspects——面向切面编程

Testing——提供了对junit或TestNG测试框架的整合。

Data Access/Integration——提供了对数据访问/集成的功能。

Spring MVC——提供了面向Web应用程序的集成功能。

一、IoC容器

IOC:inversion of Control,翻译过来是反转控制。

IoC容器

​ servlet容器(tomcat)能够管理Servlet,Filter,Listener这样的组件的一生,所以它是一个复杂容器。

​ IOC容器也是一个复杂容器,他们不仅要创建组件的对象、存储组件的对象,还要负责调用组件的方法让他们工作,最终在特定情况下销毁组件。

​ Spring的IOC容器就是IOC思想落地的产品实现。IOC容器中管理的组件也叫做bean。

​ Spring认为一切类都是Bean,比如实体类、DAO类、业务层、控制类、通知类等,容纳这些Bean的是Spring提供的IoC容器,所以Spring是一种基于Bean的编程。

IoC的API

①BeanFactory

​ 这是IOC容器的基本实现,是Spring内部使用的接口。面向Spring本身,不提供给开发人员使用。

②ApplicationContext

​ BeanFactory的子接口,提供了更多高级特性。面向Spring的使用者,几乎所有场合都使用ApplicationContext而不是底层的BeanFactory。

那么,我们在Spring环境下看到一个类或者接口的名称中包含ApplicationContext,那么基本可以断定,这个类或者接口和IoC容器有关。

几种经常使用的ApplicationContext的子类。

类型名简介
ClassPathXmlApplicationContext通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象
FileSystemXmlApplicationContext通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象
ConfigurableApplicationContextApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力。
WebApplicationContext专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。

Spring的使用

依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.1</version>
</dependency>
复制代码

配置文件【定义bean】,文件名任意,在resources目录下

<?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="happyComponent" class="com.atguigu.ioc.HappyComponentImpl"></bean>
</beans>

复制代码

IoC容器读取该配置信息,根据配置来创建对象。这就是指挥IoC容器创建对象的指令 作用: HappyComponentImpl happyComponent = new HappyComponentImpl(); 并放入IoC容器 问题1:如何从"com.atguigu.ioc.HappyComponentImpl"字符串来创建对象 使用反射: Class clazz = Class.forName("com.atguigu.ioc.HappyComponentImpl"); Object happyComponent = clazz.getConstructor().newInstance();

使用IoC方式操作

@Test
public void testWithIoC(){
    //获取IoC容器
    //BeanFactory factory = new ClassPathXmlApplicationContext("classpath:spring.xml");
    ApplicationContext factory = new ClassPathXmlApplicationContext("classpath:spring.xml");
    //从IoC容器中获取对象
    //HappyComponent happyComponent = new HappyComponentImpl();
    //HappyComponent  happyComponent = (HappyComponent)factory.getBean("happyComponent");
    HappyComponent happyComponent = factory.getBean("happyComponent", HappyComponent.class);
    //调用方法
    happyComponent.doWork();
    //优点1:不需要亲自完成对象的创建过程,哪怕非常复杂
    //缺点2:利于修改,需要修改源代码
}
复制代码

注意:MyBatis是替代JDBC访问数据库的;

SpringMVC是封装了Servlet实现Web项目的,比较具体;

Spring是比较抽象的,它的IoC和AOP关注与代码的优化,关注与解耦。

Spring基于xml的用法

所需依赖

<dependencies>
    <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- junit测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
</dependencies>

复制代码

1.创建bean

组件类

public class HappyComponent {
    
    public void doWork() {
        System.out.println("component do work ...");
    }
    
}
复制代码

配置文件【idea->new->XML Cofiguration File->Spring Config使用这个模板就可以】

<!-- 实验一 [重要]创建bean -->
<bean id="happyComponent" class="com.atguigu.ioc.component.HappyComponent"/>
复制代码

测试

public class IOCTest {
    
    // 创建 IOC 容器对象,为便于其他实验方法使用声明为成员变量
    private ApplicationContext iocContainer = new ClassPathXmlApplicationContext("applicationContext.xml");
    
    @Test
    public void testExperiment01() {
    
        // 从 IOC 容器对象中获取bean,也就是组件对象
        HappyComponent happyComponent = (HappyComponent) iocContainer.getBean("happyComponent");
    
        happyComponent.doWork();
    
    }
    
}
复制代码

2.获取bean

①通过类型.class

HappyComponent component = iocContainer.getBean(HappyComponent.class);
复制代码

根据类型来获取bean时,在满足bean唯一性的前提下,其实只是看:『对象 instanceof 指定的类型』的返回结果,只要返回的是true就可以认定为和类型匹配,能够获取到。

两个问题:

(1)如果组件类实现了接口,根据接口类型可以获取 bean 吗?

可以,前提是bean唯一

(2)如果一个接口有多个实现类,这些实现类都配置了 bean,根据接口类型可以获取 bean 吗?

不行,因为bean不唯一

②通过id

HappyComponent component = iocContainer.getBean("id名字",HappyComponent.class);
复制代码

通过id获取bean时可以指定类型,就可以不用进行强制类型转换了,否则默认是Object类型。

3.给bean的属性赋值:setter注入

<bean id="happyComponent3" class="com.atguigu.ioc.component.HappyComponent">
    
    <!-- property标签:通过组件类的setXxx()方法给组件对象设置属性 -->
    <!-- name属性:指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关) -->
    <!-- value属性:指定属性值 -->
    <property name="componentName" value="veryHappy"/>
</bean>
复制代码

4.给bean的属性赋值:引用外部以声明的bean

被引用的bean

<bean id="happyMachine" class="com.atguigu.ioc.component.HappyMachine">
    <property name="machineName" value="makeHappy"
</bean>
复制代码

引用别的bean的bean

<bean id="happyComponent4" class="com.atguigu.ioc.component.HappyComponent">
    <!-- ref 属性:通过 bean 的 id 引用另一个 bean -->
    <property name="happyMachine" ref="happyMachine"/>
</bean>
复制代码

如果错把ref属性写成了value属性,会抛出异常: Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.atguigu.ioc.component.HappyMachine' for property 'happyMachine': no matching editors or conversion strategy found 意思是不能把String类型转换成我们要的HappyMachine类型 说明我们使用value属性时,Spring只把这个属性看做一个普通的字符串,不会认为这是一个bean的id,更不会根据它去找到bean来赋值。

5.给bean的属性赋值:内部bean

<!-- 实验五 [重要]给bean的属性赋值:内部bean -->
<bean id="happyComponent5" class="com.atguigu.ioc.component.HappyComponent">
    <property name="happyMachine">
        <!-- 在一个 bean 中再声明一个 bean 就是内部 bean -->
        <!-- 内部 bean 可以直接用于给属性赋值,可以省略 id 属性 -->
        <bean class="com.atguigu.ioc.component.HappyMachine">
            <property name="machineName" value="makeHappy"/>
        </bean>
    </property>
</bean>
复制代码

6.给bean的属性赋值:引入外部属性文件

新的依赖

  <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.3</version>
        </dependency>
        <!-- 数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.31</version>
        </dependency>
复制代码

配置属性文件:

jdbc.user=root
jdbc.password=123456
jdbc.url=jdbc:mysql://192.168.198.100:3306/mybatis-example
jdbc.driver=com.mysql.jdbc.Driver
复制代码

此处使用username作为键名时,发生账号密码错误,具体原因不明。

引入spring配置文件

 <!-- 引入外部属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
复制代码

使用外部文件

<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="url" value="${jdbc.url}"/>
    <property name="driverClassName" value="${jdbc.driver}"/>
    <property name="username" value="${jdbc.user}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>
复制代码

7.给bean的属性赋值:级联属性赋值

关联对象

<bean id="happyMachine2" class="com.atguigu.ioc.component.HappyMachine"/>
复制代码

装配关联对象

<bean id="happyComponent6" class="com.atguigu.ioc.component.HappyComponent">
    <!-- 装配关联对象 -->
    <property name="happyMachine" ref="happyMachine2"/>
    <!-- 对HappyComponent来说,happyMachine的machineName属性就是级联属性 -->
    <property name="happyMachine.machineName" value="cascadeValue"/>
</bean>
复制代码

8.给bean的属性赋值:构造器注入

前提:必须要有参数完全一致的构造器

<bean id="happyTeam" class="com.atguigu.ioc.component.HappyTeam">
    <constructor-arg value="happyCorps"/>
    <constructor-arg value="10"/>
    <constructor-arg value="1000.55"/>
</bean>
复制代码

constructor-arg标签还有两个属性可以进一步描述构造器参数:

  • index属性:指定参数所在位置的索引(从0开始)
  • name属性:指定参数名

9.给bean的属性赋值:特殊值处理<![CDATA[文本]]>

字面量

<!-- 使用value属性给bean的属性赋值时,Spring会把value属性的值看做字面量 -->
<property name="commonValue" value="hello"/>
<!-- 使用ref属性给bean的属性复制是,Spring会把ref属性的值作为一个bean的id来处理 -->
<!-- 此时ref属性的值就不是一个普通的字符串了,它应该是一个bean的id -->
<property name="happyMachine" ref="happyMachine"/>
复制代码

null值

        <property name="commonValue">
            <!-- null标签:将一个属性值明确设置为null -->
            <null/>
        </property>
复制代码

CDATA节

<bean id="propValue" class="com.atguigu.ioc.component.PropValue">
    <property name="expression">
        <!-- 解决方案二:使用CDATA节 -->
        <!-- CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据 -->
        <!-- XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析 -->
        <!-- 所以CDATA节中写什么符号都随意 -->
        <value><![CDATA[a < b]]></value>
    </property>
</bean>
复制代码

10.给bean的属性赋值:使用p命名空间

前提条件:使用 p 名称空间需要导入相关的 XML 约束,在 IDEA 的协助下导入即可。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" 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 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
复制代码

使用

<bean id="happyMachine3"
      class="com.atguigu.ioc.component.HappyMachine"
      p:machineName="goodMachine"
/>
复制代码

11.给bean的属性赋值:集合属性

<bean id="happyTeam2" class="com.atguigu.ioc.component.HappyTeam">
    <property name="memberNameList">
        <!-- list标签:准备一组集合类型的数据,给集合属性赋值 -->
        <!--<list>
            <value>member01</value>
            <value>member02</value>
            <value>member03</value>
        </list>-->
        <!-- 使用set标签也能实现相同效果,只是附带了去重功能 -->
        <!--<set>
            <value>member01</value>
            <value>member02</value>
            <value>member02</value>
        </set>-->
        <!-- array也同样兼容 -->
        <array>
            <value>member01</value>
            <value>member02</value>
            <value>member02</value>
        </array>
    </property>
    <property name="managerList">
        <!-- 给Map类型的属性赋值 -->
        <!--<map>
            <entry key="财务部" value="张三"/>
            <entry key="行政部" value="李四"/>
            <entry key="销售部" value="王五"/>
        </map>-->
        <!-- 也可以使用props标签 -->
        <props>
            <prop key="财务部">张三2</prop>
            <prop key="行政部">李四2</prop>
            <prop key="销售部">王五2</prop>
        </props>
    </property>
</bean>
复制代码

总结:集合类可以使用<list>,<array>,<set>,但是值得注意的是,set是自带去重效果的。map类可以使用<map><entry key="" value=""></map>或者<props><prop></prop></props>

12.自动装配

使用bean标签的autowire属性来开启自动装配。取值有byName和byType,byType表示根据类型进行装配,此时如果类型匹配的bean不止一个,那么会抛NoUniqueBeanDefinitionExceptio,byName表示根据bean的id进行匹配。而bean的id是根据需要装配组件的属性的属性名来确定的 。

<bean id="happyService" class="com.atguigu.ioc.component.HappyService"/>
<bean id="happyService2" class="com.atguigu.ioc.component.HappyService"/>
<!-- 使用bean标签的autowire属性设置自动装配效果 -->
<bean id="happyController"
      class="com.atguigu.ioc.component.HappyController"
      autowire="byName"
>
    <!-- 手动装配:在property标签中使用ref属性明确指定要装配的bean -->
    <!--<property name="happyService" ref="happyService"/>-->
</bean>
复制代码

13.集合类型的bean

文件头

<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:util="http://www.springframework.org/schema/util"
       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/util
        http://www.springframework.org/schema/util/spring-util-4.0.xsd 
        http://www.springframework.org/schema/context 
        https://www.springframework.org/schema/context/spring-context.xsd">
复制代码

以下三条不能缺少。

<beans xmlns:util="http://www.springframework.org/schema/util"
 xsi:schemaLocation=" http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd"></beans>
复制代码

可以创建一个list类型的bean

<util:list id="machineList">
    <bean class="com.atguigu.ioc.component.HappyMachine">
        <property name="machineName" value="machineOne"/>
    </bean>
    <bean class="com.atguigu.ioc.component.HappyMachine">
        <property name="machineName" value="machineTwo"/>
    </bean>
    <bean class="com.atguigu.ioc.component.HappyMachine">
        <property name="machineName" value="machineThree"/>
    </bean>
</util:list>
复制代码

14.FactoryBean机制

实现了factoryBean的类,配置的bean,获取到的不再是它自己的对象,而是这个类的getObject()方法返回的对象。

实现了FactoryBean的类

public class HappyFactoryBean implements FactoryBean<HappyMachine> {
    
    private String machineName;
    
    public String getMachineName() {
        return machineName;
    }
    
    public void setMachineName(String machineName) {
        this.machineName = machineName;
    }
    
    @Override
    public HappyMachine getObject() throws Exception {
    
        // 方法内部模拟创建、设置一个对象的复杂过程
        HappyMachine happyMachine = new HappyMachine();
    
        happyMachine.setMachineName(this.machineName);
    
        return happyMachine;
    }
    
    @Override
    public Class<?> getObjectType() {
    
        // 返回要生产的对象的类型
        return HappyMachine.class;
    }
}
复制代码

配置

<!-- 这个bean标签中class属性指定的是HappyFactoryBean,但是将来从这里获取的bean是HappyMachine对象 -->
<bean id="happyMachine3" class="com.atguigu.ioc.factory.HappyFactoryBean">
    <!-- property标签仍然可以用来通过setXxx()方法给属性赋值 -->
    <property name="machineName" value="iceCreamMachine"/>
</bean>
复制代码

适用于创建对象过于复杂的bean,将创建过程中可能产生过多的bean的过程移回到java中编码。

15.bean的作用域

在Spring中可以通过配置bean标签的scope属性来指定bean的作用域范围,各取值含义参加下表:

取值含义创建对象的时机
singleton在IOC容器中,这个bean的对象始终为单实例IOC容器初始化时
prototype这个bean在IOC容器中有多个实例获取bean时

如果是在WebApplicationContext环境下还会有另外两个作用域(但不常用):

取值含义
request在一个请求范围内有效
session在一个会话范围内有效

配置

<!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 -->
<!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 -->
<bean id="happyMachine4" scope="prototype" class="com.atguigu.ioc.component.HappyMachine">
    <property name="machineName" value="iceCreamMachine"/>
</bean>
复制代码

基于XML的bean生命周期

Bean生命周期的主要阶段包括:

  • bean对象创建(调用无参构造器)
  • 给bean对象设置属性
  • bean对象初始化之前操作(由bean的后置处理器负责)
  • bean对象初始化(需在配置bean时指定初始化方法)
  • bean对象初始化之后操作(由bean的后置处理器负责)
  • bean对象就绪可以使用
  • bean对象销毁(需在配置bean时指定销毁方法)
  • IOC容器关闭

指定bean的初始化方法和销毁方法

①创建两个方法作为初始化和销毁方法

public void happyInitMethod() {
    System.out.println("HappyComponent初始化");
}
    
public void happyDestroyMethod() {
    System.out.println("HappyComponent销毁");
}
复制代码

②配置bean时指定初始化和销毁方法

<!-- 使用init-method属性指定初始化方法 -->
<!-- 使用destroy-method属性指定销毁方法 -->
<bean id="happyComponent"
      class="com.atguigu.ioc.component.HappyComponent"
      init-method="happyInitMethod"
      destroy-method="happyDestroyMethod"
>
    <property name="happyName" value="uuu"/>
</bean>
复制代码

bean的后置处理器

①创建后置处理器类

// 声明一个自定义的bean后置处理器
// 注意:bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行
public class MyHappyBeanProcessor implements BeanPostProcessor {
    
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    
        System.out.println("☆☆☆" + beanName + " = " + bean);
    
        return bean;
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    
        System.out.println("★★★" + beanName + " = " + bean);
    
        return bean;
    }
}
复制代码

②把bean的后置处理器放入IOC容器

<!-- bean的后置处理器要放入IOC容器才能生效 -->
<bean id="myHappyBeanProcessor" class="com.atguigu.ioc.process.MyHappyBeanProcessor"/>
复制代码

③执行效果示例

HappyComponent创建对象

HappyComponent要设置属性了

☆☆☆happyComponent = com.atguigu.ioc.component.HappyComponent@ca263c2

HappyComponent初始化

★★★happyComponent = com.atguigu.ioc.component.HappyComponent@ca263c2

HappyComponent销毁

基于注解管理bean

标记与扫描

注解的作用

①注解:

​ 和xml配置文件一样,注解本身是不能执行的,它只是作为一个标记,具体功能是框架检测到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作。

​ 本质上:所有的一切操作都是由Java代码来完成的,XML和注解只是告诉宽假中的Java代码如何执行。

②扫描

​ Spring‘为了知道程序员在什么地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根据注解进行后续操作。

1.配置注解

①@Component——普通组件

@Component
public class CommonComponent {
}
复制代码

②@Repository——持久化层组件【dao】

@Repository
public class SoldierDao {
}
复制代码

③@Service——业务逻辑组件【service】

@Service
public class SoldierService {
}
复制代码

④@Controller——控制器【controller/servlet】

@Controller
public class SoldierController {
}
复制代码

四个注解没有本质区别

​ @Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。对于Spring使用IOC容器管理这些组件来说没有区别。所以@Controller,@Service,@Repository这三个注解只是给开发人员看的,让我们便于分辨组件的作用。

​ 注意:虽然他们本质上一样,但是为了代码的可读性,为了程序结构严谨我们肯定不能随便胡乱标记。

2.扫描注解

分类讨论

①最基本的扫描方式【常用】

关键词 base-package

<context:component-scan base-package="包"/>
复制代码
②指定匹配模式

基于扫描包 base-package。

关键词 resource-pattern

<context:component-scan base-package="包" resource-pattern="Soldier*.class"/>
复制代码
③指定要排除的组件

或者也可以说指定不扫描的组件

关键词

context:exclude-filter

type

expression

<context:component-scan base-package="包">
	<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
复制代码
④只扫描指定组件

仅扫描 = 关闭默认规则 + 追加规则

关键词:use-default-filters

<!-- 仅扫描 = 关闭默认规则 + 追加规则 -->
<!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
<context:component-scan base-package="com.atguigu.ioc.component" use-default-filters="false">
    
    <!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 -->
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
复制代码

3.组件的beanName

我们在使用xml的方式管理bean的时候,每个bean都有一个唯一标识,便于在其他地方引用。现在使用注解后,每个组件仍然应该有一个唯一标识。

①默认情况

没有指定beanName 的话,类名首字母小写就是bean的id。例如:SoldierController类对应的bean的id就是soldierController。

②使用value属性指定

@Controller(value="mySoldiler")
public class SoldierController{

}
复制代码

特别的,当注解中只设置一个属性时,value属性的属性名可以省略。

@Controller("mySoldiler")
public class SoldierController{

}
复制代码

4.基于注解的自动装配

场景

  • SoldierController需要SoldierService
  • SoldierService需要SoldierDao

同时在各个组件中声明要调用的方法。

关键字

@Autowired

@Qualifier("")

实现

①前提

​ 参与自动装配的组件(需要装配别的bean的,要被装配的bean)全部都必须在IOC容器中。

②@Autowired注解

​ 在成员变量上直接标记@Autowired注解即可,不需要提供setXxx()方法。以后我们在项目的正式用法就是这样。

【1】给Controller装配Service
@Controller(value = "tianDog")
public class SoldierController {
    
    @Autowired
    private SoldierService soldierService;
    
    public void getMessage() {
        soldierService.getMessage();
    }
    
}
复制代码
【2】给Service装配Dao
@Service("smallDog")
public class SoldierService {
    
    @Autowired
    private SoldierDao soldierDao;
    
    public void getMessage() {
        soldierDao.getMessage();
    }
}
复制代码

@Autowired注解其他细节

①标记在其他位置

【1】构造器上

【2】setXxx方法上

②工作流程

@Autowired,@Qualifier("")中Qualifier注解不能单独存在,需要和AutoWired配合使用。

  1. 首先根据所需要的组件类型到IOC容器中查找
    1. 能找到唯一的bean:直接执行装配
    2. 如果完全找不到匹配这个类型的bean:装配失败
    3. 和所需类型匹配的bean不止一个
      1. 没有@Qualifier注解:根据**@Autowired标记位置成员变量的变量名作为bean的id**进行匹配。
        1. 能找到:执行装配
        2. 找不到:装配失败
      2. 有@Qualifier注解:根据**@Qualifier注解中指定的名称作为bean的id**进行匹配
        1. 能找到:执行装配
        2. 找不到:装配失败

5.完全注解开发

​ 这部分是为了springboot提供支持。在SpringBoot中,完全舍弃了XML文件。全面使用注解来完成主要的配置。

①创建配置类

使用@Configuration注解将一个普通的类标记为Spring的配置类。

import org.springframework.context.annotation.Configuration;
    
@Configuration
public class MyConfiguration {
}
复制代码

在配置类中配置bean

使用@Bean注解

@Configuration
public class MyConfiguration {
	//@Bean注解标记的方法会被放入IOC容器
    @Bean
    public CommonComponent getComponent(){
        CommonComponent commonComponent = new CommonComponent();
        commonComponent.setComponentName("created by annotation config");
        return commonComponent;
    }
    
}
复制代码

②根据配置类创建IOC容器对象

AnnotationConfigApplicationContext类

//我们之前使用的是ClassPathXmlApplicationContext实现类,是通过xml获取ioc配置信息的。
private ApplicationContext iocContainer = new ClassPathXmlApplicationContext("applicationContext.xml");
//现在使用注解配置,那么要使用AnnotationConfigApplicationContext这个实现类
private ApplicationContext iocContainerAnnotation = new AnnotationConfigApplicationContext(MyConfiguration.class);
复制代码

③在配置类中配置自动扫描的包

@ComponentScan

@configuration
@ComponentScan("com.dyy.ioc.component")
public class MyConfiguration{
//@Bean注解标记的方法会被放入IOC容器
    @Bean
    public CommonComponent getComponent(){
        CommonComponent commonComponent = new CommonComponent();
        commonComponent.setComponentName("created by annotation config");
        return commonComponent;
    }
}
复制代码

整合junit4

优点

1.不需要自己创建IOC容器了。

2.任何需要的bean都可以在测试类中直接自动装配。

操作过程

①加入依赖

同时spring-context和junit4的依赖也要加入。

<!-- Spring的测试包 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.1</version>
</dependency>
复制代码

②创建测试类

@RunWith指定Spring为Junit提供的运行器。

@ContextConfiguration(value = {"classpath:applicationContext.xml"})指定配置文件的位置。

@Autowired自动装配bean。

@Test必须要在运行的方法上面加上@Test,否则会报错。

// junit的@RunWith注解:指定Spring为Junit提供的运行器
@RunWith(SpringJUnit4ClassRunner.class)
// Spring的@ContextConfiguration指定Spring配置文件的位置
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class JunitIntegrationSpring {
    
    @Autowired
    private SoldierController soldierController;
    
    @Test
    public void testIntegration() {
        System.out.println("soldierController = " + soldierController);
    }
    
}
复制代码

二、AOP面向切面编程

代理模式

​ 二十三种设计模式中的一种,属于结构性模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类进行简介调用。让不属于目标方法互信逻辑的代码从目标方法中剥离出来,也就是解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

代理:将非核心逻辑玻璃出来以后,封装这些非核心逻辑的类、对象、方法。

目标:被代理“套用”了非核心逻辑代码的类、对象或者方法。

其实理解代理模式、AOP的核心关键词总结下来就是一个字:套。

1.静态代理

创建静态代理类:


public class CalculatorStaticProxy implements Calculator {
    
    // 将被代理的目标对象声明为成员变量
    private Calculator target;
    
    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }
    
    @Override
    public int add(int i, int j) {
    
        // 附加功能由代理类中的代理方法来实现
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
    
        // 通过目标对象来实现核心业务逻辑
        int addResult = target.add(i, j);
    
        System.out.println("[日志] add 方法结束了,结果是:" + addResult);
    
        return addResult;
    }
    ……
复制代码

总结:

静态代理确实实现了解耦。

但是代码都写死了,不够灵活,再为其他功能写附加日志时,会产生更多的重复代码。

如果将日志功能集中到一个代理类中,将来有任何需求都只能通过这一个代理类来实现。那就需要用到动态代理技术了。

2.动态代理

调用过程

创建目标->递给自定义生产代理对象工厂类->获得动态代理类对象

①生产代理对象的工厂类

JDK本身就支持动态代理,这是反射技术的一部分。

需要用到:

Proxy类——T newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h )方法

ClassLoader 类加载器

interfaces 父接口列表

InvocationHandler 自定义处理器

public class LogDynamicProxyFactory<T>{
    private T target;
    //有参构造器,接收目标
    public LogDynamicProxyFactory(T target){
      this target = target;  
    }
    //产生代理对象的方法
    public T getProxy(){
        //参数1:目标的类加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
        //参数2:目标的所有父接口
        Class<?>[] interfaces = target.getClass().getInterfaces();
        //准备第3个参数InvocationHandler,这是一个函数式接口,只有一个invoke(Object proxy,Method method,Object[] args)方法,此处使用lambda表达式来实现它。
        InvocationHandler handler = (Object proxy,Method method,Object[] args)->{
            //我们对InvocationHandler接口中invoke方法的实现就是在调用目标方法
            //围绕目标方法的调用,就可以添加我们的附加功能
            //声明一个局部变量,用来存储目标方法的返回值
            Object targetMethodReturnValue = null;
            //通过method对象获取方法名
            String methodName = method.getName();
            //为了在便于打印时看到数组中的数据,把参数数组转换为List
            List<Object> argumentList = Arrays.asList(args);
            try{
                System.out.println("[动态代理][日志]"+methodName+"方法开始了,参数是:"+argumentList);
                targetMethodReturnValue = method.invoke(target,args);
                System.out.println("[动态代理][日志]"+methodName+"方法成功结束了,返回值是:"+targetMethodReturnValue);
            }catch(Exception e){
                //通过e对象获取异常类型的全类名
                String exceptionName = e.getClass().getName();
                //目标方法失败后,打印抛出异常的日志
              System.out.println("[动态代理][日志]"+methodName+"方法发生异常,异常信息是:"+exceptionName);  
            
            }finally{
                System.out.println("[动态代理][日志]"+methodName+"方法结束了,检查资源是否关闭");
            }
            return targetMethodReturnValue;
        };
        //接口实现结束
       //调用方法产出代理,建议进行类型转换【(T)】,否则在外部调用时,每次都要进行类型转换。
        T proxy = (T)Proxy.newProxyInstance(classLoader,interfaces,handler);
        
        return proxy;
    }
}
复制代码
②测试使用
@Test
public void testDynamicProxy() {
    
    // 1.创建被代理的目标对象
    Calculator target = new CalculatorPureImpl();
    
    // 2.创建能够生产代理对象的工厂对象
    LogDynamicProxyFactory<Calculator> factory = new LogDynamicProxyFactory<>(target);
    
    // 3.通过工厂对象生产目标对象的代理对象
    Calculator proxy = factory.getProxy();
    
    // 4.通过代理对象间接调用目标对象
    int addResult = proxy.add(10, 2);
    System.out.println("方法外部 addResult = " + addResult + "\n");
    
    int subResult = proxy.sub(10, 2);
    System.out.println("方法外部 subResult = " + subResult + "\n");
    
    int mulResult = proxy.mul(10, 2);
    System.out.println("方法外部 mulResult = " + mulResult + "\n");
    
    int divResult = proxy.div(10, 2);
    System.out.println("方法外部 divResult = " + divResult + "\n");
}
复制代码

AOP概述

​ OOP:面向对象编程

​ AOP:面向切面编程 Aspect Oriented Programming

​ AOP可以说是OOP的补充和实现,OOP解决纵向的业务问题。AOP解决横向的问题。比如日志、安全验证、事务、异常。

横切关注点

纵向关注点(核心关注点):add() sub() findAll() insert() 等业务

横切关注点:日志、安全验证、事务、异常。

通知Advice

​ 就是AOP要做的事情,比如写日志,比如要进行事务处理。在Spring中,通知表现为一个方法。

​ 通知的分类:

  • 前置通知:在被代理的目标方法前执行
  • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
  • 异常通知:在被代理的目标方法异常结束后执行(死于非命)
  • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
  • 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

连接点 joinpoint

在连接点加入通知,比如add(),sub(),findAll(),在spring中,连接点是一个方法。

切入点 pointcut

切入点是一个表达式,能够涵盖所有的连接点。

如:execution(* com.atguigu.service.CalculatorImpl.*(..))

execution(* com.atguigu.service.CalculatorImpl.add(..))

切面Aspect

切面是一个类,包括两个部分:通知(干什么)+切入点(在哪里干)

目标target

被代理的目标对象。(UserDaoImpl,CalculatorPureImpl等一类实现了某些功能的类)

代理proxy(代理人+目标)

向目标对象应用统治之后创建的代理对象。

织入weave(目标+通知——>代理对象)

​ 织入指把通知应用到目标上,生成代理对象的过程。可以在编译器植入,也可以在运行期植入,Spring采用的是后者。

问题:切面和过滤器有些什么共同点和不同点

共同点:都可以用来解决横切性问题,比如日志,事务

不同点:

  1. Filter必须在Web项目下才能使用,离不开Servlet容器【tomcat服务器】,AOP没有这个要求。
  2. Filter过滤是http请求的路径,aop过滤的是方法(哪些包下哪些类的哪些方法)

示例:添加日志(准备)

1.添加依赖

<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.1</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- Spring的测试包 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.1</version>
</dependency>
复制代码

2.定义目标接口和目标类

public interface Calculator {
    
    public int add(int i, int j);
    
    int sub(int i, int j);
    
    int mul(int i, int j);
    
    int div(int i, int j);
}
复制代码
@Component
public class CalculatorPureImpl implements Calculator {
.....
}

复制代码

3.配置(扫描注解)

<context:component-scan base-package="com.atguigu.service"></context:component-scan>
复制代码

4.测试(Spring整合JUnit4)

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class TestAOP {
    @Autowired
    private Calculator calculator;

    @Test
    public void testCalculator(){
        int result = calculator.add(10, 20);
        System.out.println(result);

        int result2 = calculator.sub(10,20);
        System.out.println(result2);
    }
}
复制代码

目前并没有使用到AOP。

但是我们希望给Calculator的方法添加日志。此时我们使用AOP就完全可以在不修改代码的基础上,添加日志功能。

示例:添加日志(实现)

1.添加依赖

   <!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.1</version>
</dependency>
复制代码

2.定义切面(通知+切入点)

@Aspect

@Component必须添加,AOP的实现离不开IOC容器。

//这是一个切面
@Aspect
//要创建Bean并放入到IoC容器中
@Component
public class LogAspect {

    @Before("execution(public int com.atguigu.service.CalculatorPureImpl.*(int,int))")
     public void beforeLog(){
         System.out.println("[日志]:方法开始执行,参数是:");
     }
     @AfterReturning("execution(public int com.atguigu.service.CalculatorPureImpl.*(int,int))")
     public void afterSuccessLog(){
         System.out.println("[日志]:方法执行成功结束,结果是:");
     }
    @AfterThrowing("execution(public int com.atguigu.service.CalculatorPureImpl.*(int,int))")
     public  void afterExceptionLog(){
         System.out.println("[日志]:方法执行异常结束,异常是:");
     }
    @After("execution(public int com.atguigu.service.CalculatorPureImpl.*(int,int))")
     public void afterFinallyLog(){
         System.out.println("[日志]:方法执行结束Finally,执行最后的扫尾操作:");
     }
}
复制代码

切面是一个类:这里是我们自定义的LogAspect。

切面有两个部分组成:

通知,@Before @After @AfterReturning @AfterThrowing 必须写到一个方法上

切入点:是一个表达式

execution(public int com.atguigu.service.CalculatorPureImpl.*(int,int))

切面类LogAspect和业务类Calculator完全解耦,借助切入点表达式建立练习。

3.配置文件中启动AOP

<aop:aspectj-autoproxy>打开AOP的自动代理(动态代理)

<!-- 扫描IoC的注解Annotation:@Component @Service @Resposity @Controller-->
<context:component-scan base-package="com.atguigu.service,com.atguigu.aspect"></context:component-scan>
<!-- 启用AOP的自动代理-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

复制代码

示例:重用切入点

@PointCut

切面就是一个类,由两部分组成:通知(四个方法,需要添加通知注解,说明通知类型)+切入点(是一个表达式,可以重用)

public class MyPointcut {
    @Pointcut("execution(public int com.atguigu.service.CalculatorPureImpl.div(int,int))")
    public void pointcut1(){

    }
}
复制代码
/这是一个切面
@Aspect
//要创建Bean并放入到IoC容器中
@Component
public class LogAspect {

    @Before(value = "MyPointcut.pointcut1()")
     public void beforeLog(){
         System.out.println("[日志]:方法开始执行,参数是:");
     }
     @AfterReturning("MyPointcut.pointcut1()")
     public void afterSuccessLog(){
         System.out.println("[日志]:方法执行成功结束,结果是:");
     }
    @AfterThrowing("MyPointcut.pointcut1()")
     public  void afterExceptionLog(){
         System.out.println("[日志]:方法执行异常结束,异常是:");
     }
    @After("MyPointcut.pointcut1()")
     public void afterFinallyLog(){
         System.out.println("[日志]:方法执行结束Finally,执行最后的扫尾操作:");
     }
}
复制代码

AOP注解方式

1.在通知中获取连接点细节信息

需要用到JoinPoint对象。

//这是一个切面
@Aspect
//要创建Bean并放入到IoC容器中
@Component
public class LogAspect {
    @Before(value = "MyPointcut.pointcut1()")
     public void beforeLog(JoinPoint joinPoint){ //add(int int)  sub(int int)
        Object[] args = joinPoint.getArgs();
        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();//方法名 add
        int modifiers = signature.getModifiers(); //修饰符 public
        String declaringTypeName = signature.getDeclaringTypeName();//方法属于哪个类
        //joinPoint.getTarget();  //目标对象
        //joinPoint.getThis();//代理对象
       // System.out.println("[日志]:"+modifiers+"  "+ Modifier.toString(modifiers)+" "+declaringTypeName);

        System.out.println("[日志]:"+methodName+"方法开始执行,参数是:"+ Arrays.toString(args));
     }
     @AfterReturning(value = "MyPointcut.pointcut1()",returning = "result1")
     public void afterSuccessLog(JoinPoint joinPoint,Object result1){
         String methodName = joinPoint.getSignature().getName();
         System.out.println("[日志]:"+methodName+"方法执行成功结束,结果是:"+result1);
     }
    @AfterThrowing(value = "MyPointcut.pointcut1()",throwing="ex")
     public  void afterExceptionLog(Exception ex){
         System.out.println("[日志]:方法执行异常结束,异常是:"+ex);
     }
    @After("MyPointcut.pointcut1()")
     public void afterFinallyLog(){
         System.out.println("[日志]:方法执行结束Finally,执行最后的扫尾操作:");
     }
}
复制代码

2.切入点的详细语法

  • 切入点是某包某类的无参数的方法,比如:execution(* com.dyy.service.impl.Student.test())

  • 切入点是某包某类带有参数的方法,示例: execution(* com.dyy.service.impl.Student.test(String,int))

  • 切入点是某包某类的某个同名的所有方法,示例:.. 表示任意个数任意类型的参数execution(* com.dyy.service.impl.Student.test(..))

  • 切入点是某报下的某类的所有方法示例:*表示任意的类名,方法名,包名,execution(* com.dyy.service.impl.Student.*(..))

  • 切入点是某包下的所有类的所有方法,示例:* 表示任意的类名,方法名,包名

    execution(* com.dyy.service.impl.* . *(..))

注意:

  • *如何通配public int?

可以是public **,但是不能是* int,这种表达式是错误的。

  • 如何统配任意的一级包和多级包?

一级包:*.

多级包:*..

  • 如何统配没有参数的方法和任意参数方法?

没有参数的方法:()

任意参数的方法:(..)

3.环绕通知

注意:使用的是@Around【环绕通知】注解的话,想要获取连接点信息需要使用ProceedingJoinPoint而不是JoinPoint,这是因为ProceedingJoinPoint有proceed()方法,能够调用下一个切面。

//这是一个切面
@Aspect
//创建Bean对象放入IoC容器
@Component
public class TransactionAspect {
    @Around("execution(* com.atguigu.service.CalculatorPureImpl.*(..))")
    public Object manageTransaction(ProceedingJoinPoint pjp){
        Object result = null;
        try{
            //调用目标对象之前通知:前置通知
            System.out.println("[事务] 开启事务  conn.setAutoCommit(false) ");
            //调用目标对象的方法
            result = pjp.proceed(); // 调用下一个切面或者目标方法
            //特别像过滤器中 chain.doFilter(request,response)
            //调用目标对象成功结束之后通知:返回通知
            System.out.println("[事务] 提交事务  conn.commit()");
        }catch(Throwable e){
            //调用目标对象异常结束后通知:异常通知
            System.out.println("[事务] 回滚事务  conn.rollback()");
        }finally{
            //不管成功失败都执行的通知:后置通知
            System.out.println("[事务] 不管提交回滚都需要执行该操作");
        }

        //返回目标方法的执行结果
        return result;
    }
}


复制代码

4.切面优先级

想要指派和确定各个切面的执行顺序,需要通过@Order来指定。这个值越小越早执行

//这是一个切面
@Aspect
//创建Bean对象放入IoC容器
@Component
@Order(4)
public class TransactionAspect {
}

复制代码
//这是一个切面
@Aspect
//要创建Bean并放入到IoC容器中
@Component
@Order(3)
public class LogAspect {


复制代码

执行优先级:LogAspect>TransactionAspect,执行过程是LogAspect在外层【先开始后结束】,TransactionAspect在里层【后开始先结束】。

5.没有接口的情况

  • 情况1:没有切面
  • 情况2:使用切面,但是有接口(底层使用的是JDK的动态代理),比如前面写的计算器接口public class CalculatorPureImpl implements Calculator
  • 情况3:使用切面,但是没有接口(底层使用的是CGLIB)
  • 情况4:使用切面,不管有没有接口,都使用CGLIB。

总结

JDK动态代理有前提,代理的对象的类必须有接口。

CGLIB动态代理,有没有接口都可以。

设置切面只由CGLIB实现

在spring配置文件中设置。

<aop:aspectj-autoproxy proxy-target-class="true"/>
复制代码

proxy-target-class属性为true,就是说不管是否有接口,都指定CGLIB实现AOP。

属性为false【默认值】,有接口就使用JDK动态代理,没有接口就使用CGLIB。

为什么JDK动态代理必须有接口呢?

​ 这是因为JDK动态代理生成的代理类已经继承了Proxy,而Java是单继承的,所以必须有接口。

为什么CGLIB实现动态代理不要求有接口?

static{ System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "cglib"); }这是由于CGLIB生成的代理类没有固定的父类,所以可以直接继承目标类,不需要接口。

AOP对IoC的影响

情况1:没有AOP,使用接口和实现类都可以(前提,使用接口,接口只有一个实现类;使用实现类,实现类只能在SpringIoC容器中有一个实例)

public class TestAOP2 {
    @Autowired
    //private Calculator calculator;
    private CalculatorPureImpl calculator;
}
复制代码

情况2:使用AOP,但是使用的是JDK动态代理(Proxy) <aop:aspectj-autoproxy proxy-target-class="false"/>

spring-1.png 使用接口可以,使用实现类不可以

情况3:使用AOP,但是使用的是CGLIB动态代理<aop:aspectj-autoproxy proxy-target-class="true"/>使用接口,实现类都可以.

spring-2.png

AOP的xml配置方式

了解即可,目前spring的开发以注解开发为主。xml配置基本不使用。

<!--@Component -->
<bean id="calculatorPure" class="com.atguigu.service.CalculatorPureImpl"></bean>

<bean id="logAspect" class="com.atguigu.aspect.LogAspect"></bean>

<aop:config>
    <!--切入点 @Pointcut -->
    <aop:pointcut id="pointcut1" expression="execution(* com.atguigu.service.*.*(..))"/>

    <!--@Aspect -->
    <!--@Order -->
    <aop:aspect ref="logAspect" order="3">
        <!--@Before -->
        <aop:before method="beforeLog" pointcut-ref="pointcut1"></aop:before>
        <!-- @AfterReturning-->
        <aop:after-returning method="afterSuccessLog" pointcut-ref="pointcut1" returning="result1"></aop:after-returning>
        <!-- @AfterThrowing-->
        <aop:after-throwing method="afterExceptionLog" pointcut-ref="pointcut1" throwing="ex"></aop:after-throwing>
        <!--@After -->
        <aop:after method="afterFinallyLog" pointcut-ref="pointcut1"></aop:after>
    </aop:aspect>        
</aop:config>

复制代码

JDBCTemplate

​ spring提供了整合JDBC的JDBCTemplate,因为先有Spring,再出现MyBatis,所以整合包不是由spring提供的,而是第三方提供的。

​ JDBCTemplate和MyBatis的共同点:

  • 都是对JDBC进行了封装

JDBCTemplate和MyBatis的不同点:

  • MyBatis封装的更加彻底,提供的功能更丰富。

示例

1.添加依赖

spring-orm

Spring在执行持久化层操作、与持久化层技术进行整合的过程中,需要使用到orm,jdbc,tx三个jar包。我们导入orm包就可以通过Maven的依赖传递性把其他两个也导入了。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>5.3.1</version>
</dependency>
复制代码
2.进行配置
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driver}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.user}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"></property>
</bean>
复制代码
3.具体操作

JDBCTemplate的API

增删改

@Test
public void testInsert() throws SQLException {
    String sql = "insert into t_emp values(null,?,?)";
    jdbcTemplate.update(sql,"johnson",400.45);
}
@Test
public void testUpdate() throws SQLException {
    String sql = "update t_emp set emp_name = ? ,emp_salary = ? where emp_id = ?";
    jdbcTemplate.update(sql,"johnson2",800.45,31);
}
@Test
public void testDelete() throws SQLException {
    String sql = "delete from t_emp where emp_id = ?";
    jdbcTemplate.update(sql,31);
}
@Test
public void testQueryName() throws SQLException {
    String sql = "select emp_name from t_emp where emp_id =1";
    String ename = jdbcTemplate.queryForObject(sql, String.class);
    System.out.println(ename);
}

复制代码

查询

@Test
public void testQueryEntity() throws SQLException {
    String sql = "select * from t_emp where emp_id =1";
    //BeanPropertyRowMapper:按照驼峰命名规则进行映射 emp_id ----> empId  emp_salary-->empSalary
    RowMapper<Employee> rowMapper = new BeanPropertyRowMapper(Employee.class);
    Employee emp = jdbcTemplate.queryForObject(sql, rowMapper);
    System.out.println(emp);
}
@Test
public void testQueryList() throws SQLException {
    String sql = "select * from t_emp";
    //BeanPropertyRowMapper:按照驼峰命名规则进行映射 emp_id ----> empId  emp_salary-->empSalary
    RowMapper<Employee> rowMapper = new BeanPropertyRowMapper(Employee.class);
    List<Employee> list = jdbcTemplate.query(sql, (ResultSet rs, int var2)->{
            Employee emp = new Employee();
            emp.setEmpId(rs.getInt(1));
            emp.setEmpName(rs.getString("emp_name"));
            emp.setSalary(rs.getDouble("emp_salary"));
            return emp;
    });
   list.forEach((emp)-> System.out.println(emp));
}

复制代码

函数

@Test
public void testCount() throws SQLException {
    String sql = "select count(*) from t_emp";
    Integer count = jdbcTemplate.queryForObject(sql,Integer.class);
    System.out.println(count);
}


复制代码

三、声明式事务

事务类型

  • 编程式事务
  • 声明式事务(xml声明,注解声明)

spring的事务管理的API.

  • TransactionStatus getTransaction(@Nullable TransactionDefinition var1)
  • void commit(TransactionStatus var1)
  • void rollback(TransactionStatus var1)

spring-3.png 我们现在(使用JDBCTemplate访问数据库)要使用的事务管理器是org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合 Mybatis 用的也是这个类。

DataSourceTransactionManager类中的主要方法:

  • doBegin():开启事务
  • doSuspend():挂起事务
  • doResume():恢复挂起的事务
  • doCommit():提交事务
  • doRollback():回滚事务

事务实验

1添加依赖

和JdbcTemplate相同的依赖:必须有spring-orm。

2配置文件

和JdbcTemplate相同的配置

3DAO层代码

EmpDao

4业务代码

EmpService

5测试代码

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring.xml")
public class TestTransaction {
    @Autowired
    private EmpService empService;
    @Test
    public void testUpdate(){
        empService.updateTwice(1,"tomcat",2,800.0);
    }
}

复制代码

6配置事务管理器

用到的类是DataSourceTransactionManager

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

复制代码

7开启基于注解的声明式事务功能

开启扫描事务注解

<!--启动事务的注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>

复制代码

8在需要事务的方法上使用注解

事务加载业务层。要在方法上方

9启用日志

(1)logback依赖

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

复制代码

(2)日志文件的配置

文件名声明为logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    <!-- 指定日志输出的位置 -->
    <appender name="STDOUT"
              class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- 日志输出的格式 -->
            <!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 -->
            <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern>
        </encoder>
    </appender>

    <!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR -->
    <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 -->
    <root level="INFO">
        <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender -->
        <appender-ref ref="STDOUT" />
    </root>

    <!-- 根据特殊需求指定局部日志级别 -->
    <logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager" level="DEBUG"/>
    <logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG" />

</configuration>


复制代码

完成。

完成的功能:成功提交事务;失败就回滚事务。

事务实验:只读

public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    String[] label() default {};

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    String timeoutString() default "";

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

复制代码

事务的四大特征:ACID 原子性 一致性 隔离性 持久化

Spring中事务的四大属性:

Propagation 传播特性

Isolation 隔离级别

timeout 超时时间

readonly只读事务

对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。

针对增删改操作设置只读,就会抛出异常:Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

问题:查询不使用事务, 还是使用只读事务??

应用场合:

如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;

如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。

问题:注解@Transactional的位置

类上:该类的所有方法都启用事务

方法上:只有该方法启用事务,和其他方法无关

如果类上和某些方法上都有事务,以方法上的事务配置为准。

事务实验:超时

@Transaction(timeout=5)
public void updateTwice(Employee e){}
复制代码
  1. 如果事务中后一个操作超时,事务会回滚,前一个操作也回滚。
  2. 如果超时,出现如下org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2021
  3. Thread.sleep()要注意位置,如果添加在上图的位置,即使超时,也不会回滚.

事务实验:回滚异常类型

默认是只针对运行时异常回滚,编译时异常不回滚。

设置所有异常都回滚

方法1:设置rollbackFor=Exception.class

@Transactional(timeout = 5,
rollbackFor = Exception.class
)

复制代码

方法2:异常链。底层出现了检查异常,进行处理,并向上层抛出一个新的异常(一般是运行时异常)。既可以传递异常到上层,还可以避免方法签名中使用throws。在Spring事务中还可以实现所有异常都回滚的效果。

public void updateEmpSalaryById(Integer empId, Double salary)  {
    String sql = "update t_emp set emp_salary=? where emp_id=?";
    jdbcTemplate.update(sql, salary, empId);

    //int n = 10/0;
    //new FileInputStream("asdfadf");
    try {
        new FileInputStream("asdfadf");
    } catch (FileNotFoundException e) {
        e.printStackTrace();
        throw new RuntimeException(e.getMessage());
    }
}

复制代码

事务实验:隔离界别

1.数据库访问的并发问题

  • 脏读:一个事务读到了另一个事务的未提交的数据。
  • 不可重复读:一个事务读取到了另一个事务已经提交的update的数据导致多次查询结果不一致。
  • 幻读:一个事务督导了另一个事务已经提交的insert或者delete的数据导致多次查询结果不一致。

2.通过设置不同的隔离级别解决数据库访问的并发问题

隔离级别脏读不可重复读幻读
READ_UNCOMMITTED
READ_COMMITTED×
REPEATABLE_READ××
SERIALIZABLE×××

3.Spring的隔离级别

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),    READ_COMMITTED(2),    REPEATABLE_READ(4),    SERIALIZABLE(8);
    private final int value;
    private Isolation(int value) {
        this.value = value;
    }
    public int value() {
        return this.value;
    }
}

复制代码

​ 如果选择DEFAULT,默认值,由底层数据库自动判断应该使用什么隔离级别。

​ 对于互联网高并发项目来说,如采用隔离级别SERIALIZABLE【串行化】,固然安全,但是性能会受到严重影响。此时一般将隔离级别降低,保证效率,在配合悲观锁,乐观锁等技术保证安全性。

​ 事务的隔离级别要得到底层数据库引擎的支持,而不是应用程序或者框架的支持。Oracle支持的2中的事务隔离界别:READ_COMMITED(默认,读已提交),SERIALIZABLE。MySQL支持四种事务隔离级别,默认是REPEATABLE READ(可重复读)。

注意:MySQL中使用了MVCC多版本控制技术,在REPEATABLE READ这个级别也可以避免幻读。

事务实验:传播特性

​ 在大多数情况下,我们认为事务操作要么都成功,要么回到初始状态。比如下订单,如果主订单添加成功,三个订单明细添加成功两个,总额就对不上了,顾客最终收获会有减少。这种情况肯定要保证全部成功。类似还有银行转账例子。

​ 但也存在不同的需求,一般是业务层直接调用了DAO层,不存在业务层平行调用的问题。那在什么情况下会有被调用的方法是否需要开启事务的问题呢?

比如说:

  • Service方法应用了通知
  • 过滤器或者拦截器等类似组件。
名称含义
REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是默认值。
REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。
NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
NESTED如当前存在事务,则在嵌套事务内执行。如当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作

四、Spring5新特性

JSR 305注解

1.JCP

​ JCP是一个由SUN公司发起的,开放的国际组织。主要由Java开发者一级被授权者组成,负责Java技术规范维护,Java技术发展和更新。

2.JSR

​ JSR的全称是:Java Specification Request,意思是Java规范提案,然和人都可以向JCP提出新增一个标准化技术规范的正式请求。

3.JSR305

​ 主要功能:使用注解(例如@NonNull等等)协助开发者侦测软件缺陷。

​ Spring从5.0版本开始支持了JSR305规范中涉及到的相关注解。

整合Junit5

1.添加依赖

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>

复制代码

2.整合Junit5测试

@SpringJUnitConfig(locations = "classpath:spring.xml")
//@ExtendWith(SpringExtension.class)
//@ContextConfiguration(value = {"classpath:spring.xml"})
public class TestTransaction {

    @Autowired
    private EmpService empService;

    @Test
    public void testUpdateTwice() throws FileNotFoundException {
        //empService.updateTwice(1,"tomcat",2,600.0);
        empService.updateTwice(1,"tomcat44",2,444.44);
    }
}

复制代码

JUnit4:org.junit.Test

JUnit5:import org.junit.jupiter.api.Test

3.JUnit4和JUnit5的差别

特征JUNIT 4JUNIT 5
声明一种测试方法@Test@Test
在当前类中的所有测试方法之前执行@BeforeClass@BeforeAll
在当前类中的所有测试方法之后执行@AfterClass@AfterAll
在每个测试方法之前执行@Before@BeforeEach
每种测试方法后执行@After@AfterEach
禁用测试方法/类@Ignore@Disabled
测试工厂进行动态测试NA@TestFactory
嵌套测试NA@Nested
标记和过滤@Category@Tag
注册自定义扩展NA@ExtendWith
分类:
后端
标签:
分类:
后端
标签: