Java笔记-----(5)Spring 框架

Java笔记-----(5)Spring 框架

Spring是目前流行的一站式框架,包括SpringMVC和SpringBoot都给我们搭建Web系统提供了便利。

Spring是一个轻量级的IOC和AOP容器框架。是为Java应用程序提供基础服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。
Spring的核心模块如下所示:

  • Spring Core:是核心类库,提供IOC服务;
  • Spring Context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等);
  • Spring AOP:提供AOP服务;
  • Spring DAO:对JDBC进行了抽象,简化了数据访问异常等处理;
  • Spring ORM:对现有的ORM持久层框架进行了支持;
  • Spring Web:提供了基本的面向Web的综合特性;
  • Spring MVC:提供面向Web应用的Model-View-Controller实现。

Spring的优点:

  • Spring的依赖注入将对象之间的依赖关系交给了框架来处理,减小了各个组件之间的耦合性;
  • AOP面向切面编程,可以将通用的任务抽取出来,复用性更高;
  • Spring对于其余主流框架都提供了很好的支持,代码的侵入性很低。

(1)Spring中的控制反转(IOC)

IOC(Inversion Of Control)也叫控制反转,将对象间的依赖关系交给Spring容器,使用配置文件来创建所依赖的对象,由主动创建对象改为了被动方式,实现解耦合

在Spring配置文件中配置 <context:annotation-config/>元素开启注解。还有一个概念是DI(Dependency Injection)(依赖注入),和控制反转是同一个概念的不同角度的描述,即应用程序在运行时依赖IOC容器来动态注入对象需要的外部资源(对象等)

IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。

(1.1)将一个类声明为Spring的bean的注解

可以通过注解@Autowired@Resource来注入对象,被注入的对象必须被下边的四个注解之一标注:

  • @Controller
    一般用于表现层的注解
    对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面
  • @Service
    一般用于业务层的注解
    对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层
  • @Repository
    一般用于持久层的注解
    对应持久层即 Dao 层,主要用于数据库相关操作
  • @Component
    把资源让 spring 来管理。相当于在 xml 中配置一个 bean 通用的注解,可标注任意类为 Spring 组件。如果一个Bean不知道属于哪个层,可以使用@Component 注解标注

(1.2)@Component 和 @Bean 的区别

  • 作用对象不同: @Component 注解作用于类,而@Bean注解作用于方法。
  • @Component通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用 @ComponentScan
    注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。
    @Bean注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了Spring这是某个类的实例,当我需要用它的时候还给我。
  • @Bean 注解比 Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。

@Bean注解使用示例:

@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }
}

相当于下面的 xml 配置
<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

通过 @Component 无法实现的情况:

@Bean
public OneService getService(status) {
    case (status)  {
        when 1:
                return new serviceImpl1();
        when 2:
                return new serviceImpl2();
        when 3:
                return new serviceImpl3();
    }
}

(2)Spring中的面向切面编程(AOP)

AOP(Aspect Oriented Programming),面向切面编程是指当需要在某一个方法之前或者之后做一些额外的操作,比如说日志记录,权限判断,异常统计等,可以利用AOP将功能代码从业务逻辑代码中分离出来

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性

AOP相关术语:

  • Joinpoint(连接点): 类里面可以被增强的方法,这些方法称为连接点
  • Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义
  • Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知
  • Aspect(切面):是切入点和通知(引介)的结合
  • Introduction(引介):引介是一种特殊的通知,在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或属性
  • Target(目标对象):代理的目标对象(要增强的类)
  • Weaving(织入):是把增强应用到目标的过程,把advice 应用到 target的过程
  • Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类

简单理解:

  • 切入点就是在类里边可以有很多方法被增强,比如实际操作中,只是增强了个别方法,则定义实际被增强的某个方法为切入点;
  • 通知/增强 就是指增强的逻辑,比如扩展日志功能,这个日志功能称为增强;
  • 切面就是把增强应用到具体方法上面的过程称为切面。

(2.1)Spring AOP 和 AspectJ AOP 的区别

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。

(2.2)Spring AOP中的代理模式

代理模式分为静态代理和动态代理:

  • 静态代理
    字节码一上来就创建好,并完成加载
    装饰者模式就是静态代理的一种体现
  • 动态代理:
    字节码随用随创建,随用随加载
    不修改源码的基础上对方法增强

Spring中的AOP主要有两种实现方式:

① 基于接口的动态代理

提供者:使用JDK动态代理实现,使用java.lang.reflection.Proxy类来处理
要求:被代理类最少实现一个接口

JDK动态代理,只能对实现了接口的类生成代理,而不是针对类,该目标类型实现的接口都将被代理。原理是通过在运行期间创建一个接口的实现类来完成对目标对象的代理。

public interface IProducer {

    /**
     * 销售
     * @param money
     */
    public void saleProduct(float money);

    /**
     * 售后
     * @param money
     */
    public void afterService(float money);
}
public class Producer implements IProducer{

    /**
     * 销售
     * @param money
     */
    public void saleProduct(float money){
        System.out.println("销售产品,并拿到钱:"+money);
    }

    /**
     * 售后
     * @param money
     */
    public void afterService(float money){
        System.out.println("提供售后服务,并拿到钱:"+money);
    }
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Client {

    public static void main(String[] args) {

		//这里必须是final吗?
        final Producer producer = new Producer();

        /**
         *  newProxyInstance方法的参数:
         *      ClassLoader:类加载器
         *          它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
         *      Class[]:字节码数组
         *          它是用于让代理对象和被代理对象有相同方法。固定写法。
         *      InvocationHandler:用于提供增强的代码
         *          它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
         *          此接口的实现类都是谁用谁写。
         */
        IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(
        	producer.getClass().getClassLoader(),
            producer.getClass().getInterfaces(),
            new InvocationHandler() {
                /**
                 * 作用:执行被代理对象的任何接口方法都会经过该方法
                 * 方法参数的含义
                 * @param proxy   代理对象的引用
                 * @param method  当前执行的方法
                 * @param args    当前执行方法所需的参数
                 * @return        和被代理对象方法有相同的返回值
                 * @throws Throwable
                 */
    
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    //提供增强的代码
                    Object returnValue = null;
                    //1.获取方法执行的参数
                    Float money = (Float)args[0];
                    //2.判断当前方法是不是销售
                    if("saleProduct".equals(method.getName())) {
                        returnValue = method.invoke(producer, money*0.8f);
                    }
                    return returnValue;
                }
            });

        proxyProducer.saleProduct(10000f);
    }
}

② 基于子类的动态代理

提供者:使用第三方的cglib来实现,如果报 asmxxxx 异常,需要导入 asm.jar
要求:被代理类不能用 final 修饰的类(最终类)

cglib主要针对类实现代理,对是否实现接口无要求。原理是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以被代理的类或方法不可以声明为final类型

public class Producer {

    /**
     * 销售
     * @param money
     */
    public void saleProduct(float money){
        System.out.println("销售产品,并拿到钱:"+money);
    }

    /**
     * 售后
     * @param money
     */
    public void afterService(float money){
        System.out.println("提供售后服务,并拿到钱:"+money);
    }
}

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class Client {

    public static void main(String[] args) {
        
        final Producer producer = new Producer();

        /**
         * 动态代理:
         *  特点:字节码随用随创建,随用随加载
         *  作用:不修改源码的基础上对方法增强
         *  分类:
         *      基于接口的动态代理
         *      基于子类的动态代理
         *  基于子类的动态代理:
         *      涉及的类:Enhancer
         *      提供者:第三方cglib库
         *  如何创建代理对象:
         *      使用Enhancer类中的create方法
         *  创建代理对象的要求:
         *      被代理类不能是最终类
         *  create方法的参数:
         *      Class:字节码
         *          它是用于指定被代理对象的字节码。
         *
         *      Callback:用于提供增强的代码
         *          它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
         *          此接口的实现类都是谁用谁写。
         *          我们一般写的都是该接口的子接口实现类:MethodInterceptor
         */
        Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * 执行被代理对象的任何方法都会经过该方法
             * @param proxy
             * @param method
             * @param args
             *    以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
             * @param methodProxy :当前执行方法的代理对象
             * @return
             * @throws Throwable
             */
            
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                //提供增强的代码
                Object returnValue = null;

                //1.获取方法执行的参数
                Float money = (Float)args[0];
                //2.判断当前方法是不是销售
                if("saleProduct".equals(method.getName())) {
                    returnValue = method.invoke(producer, money*0.8f);
                }
                return returnValue;
            }
        });
        cglibProducer.saleProduct(12000f);
    }
}

Spring AOP对这两种代理方式的选择:

  • 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP,也可以强制使用cglib实现AOP
  • 如果目标对象没有实现接口必须采用cglib库,Spring会自动在JDK动态代理和cglib之间转换。

Spring AOP之动态代理


(3)IOC容器的初始化过程

在这里插入图片描述

1.Resource资源定位:
Resouce定位是指BeanDefinition的资源定位,也就是IOC容器找数据的过程。Spring中使用外部资源来描述一个Bean对象,IOC容器第一步就是需要定位Resource外部资源。由ResourceLoader通过统一的Resource接口来完成定位。

2.BeanDefinition的载入:
载入过程就是把定义好的Bean表示成IOC容器内部的数据结构,即BeanDefinition在配置文件中每一个Bean都对应着一个BeanDefinition对象
通过BeanDefinitionReader读取,解析Resource定位的资源,将用户定义好的Bean表示成IOC容器的内部数据结构BeanDefinition
在IOC容器内部维护着一个BeanDefinitionMap的数据结构,通过BeanDefinitionMap,IOC容器可以对Bean进行更好的管理。

3.BeanDefinition的注册:
注册就是将前面的BeanDefition保存到Map中的过程,通过BeanDefinitionRegistry接口来实现注册。

加深理解可以阅读:
自己动手实现的 Spring IOC 和 AOP - 上篇

IOC容器的初始化过程就是对Bean定义资源的定位、载入和注册,此时容器对Bean的依赖注入并没有发生

(3.1)依赖注入的发生时刻

ApplicationContext默认会在容器启动的时候创建我们配置好的各个Bean,我们的Bean配置如下:

<bean id="oneBean" class="com.nowcoder.oneBean">

这里边的隐藏属性是lazy-init,即上边的配置和下边的是一样的:

<bean id="oneBean" class="com.nowcoder.oneBean"  lazy-init="false">

lazy-init=false表示不开启延迟加载在容器启动的时候即创建该Bean。对应的,我们还可以配置lazy-init=true表示开启延迟加载,那么该Bean的创建发生在应用程序第一次向容器索取Bean时,通过getBean()方法的调用完成。

(3.2)BeanFactory和FactoryBean的区别

  • BeanFactory:
    Bean工厂,是一个工厂(Factory), 是Spring IOC容器的最顶层接口,它的作用是管理Bean,即实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。
  • FactoryBean:
    工厂Bean,是一个Bean,作用是产生其他Bean实例,需要提供一个工厂方法,该方法用来返回其他Bean实例。

在这里插入图片描述

(3.3)BeanFactory和ApplicationContext的区别

在这里插入图片描述

BeanFactory是Spring里面最顶层的接口,包含了各种Bean的定义,读取Bean配置文档,管理Bean的加载、实例化,控制Bean的生命周期,维护Bean之间的依赖关系

ApplicationContext接口是BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:

  • 继承了MessageSource,支持国际化。
  • 提供了统一的资源文件访问方式。
  • 提供在Listener中注册Bean的事件。
  • 提供同时加载多个配置文件的功能。
  • 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。

ApplicationContext 几种常见的实现方式:

  • FileSystemXmlApplicationContext:此容器从一个XML文件中加载Bean的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数。
  • ClassPathXmlApplicationContext:此容器也从一个XML文件中加载Bean的定义,需要正确设置classpath因为这个容器将在classpath里找Bean配置。(推荐)
  • WebXmlApplicationContext:此容器加载一个XML文件,定义了一个WEB应用的所有Bean。
  • AnnotationConfigApplicationContext:当我们使用注解配置容器对象时,需要使用此类来创建 spring 容器。它用来读取注解。(传智)

在创建Bean和内存占用方面的区别:

  • BeanFactory采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,就不能发现一些存在于Spring配置中的问题。如果Bean的某一个属性没有注入,BeanFactory加载后,直至第一次使用调用getBean方法才会抛出异常。
  • ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例Bean ,确保当需要的时候,可以直接获取。
  • 相对于基本的BeanFactory,ApplicationContext不足之处是占用内存空间。当应用程序配置Bean较多时,程序启动较慢,因为其一次性创建了所有的Bean。

BeanFactory和ApplicationContext的优缺点分析:
BeanFactory的优缺点:
优点:应用启动的时候占用资源很少,对资源要求较高的应用,比较有优势;
缺点:运行速度会相对来说慢一些。而且有可能会出现空指针异常的错误,而且通过Bean工厂创建的Bean生命周期会简单一些。

ApplicationContext的优缺点:
优点:所有的Bean在启动的时候都进行了加载,系统运行的速度快;在系统启动的时候,可以发现系统中的配置问题
缺点:把费时的操作放到系统启动中完成,所有的对象都可以预加载,缺点就是内存占用较大

(4)Spring中Bean的作用域

  • singleton : Bean在每个Spring IOC 容器中只有一个实例,默认作用域。
  • prototype:一个Bean的定义可以有多个实例。
  • request每次HTTP请求都会创建一个Bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。
  • session在一个HTTP Session中,一个Bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
  • global-session在一个全局的HTTP Session中,一个Bean对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。

(4.1)Spring 中的单例 bean 的线程安全问题

Q:Spring中Bean的作用域常使用的是singleton,也就是单例作用域。那么单例Bean是线程安全的吗?

A: 单例Bean和线程安全与否没有必然的关系。多个线程在多个工作内存和主内存交互的时候会出现不一致的地方,那么就不是线程安全的。大部分的Spring Bean并没有可变的状态(比如Service类和DAO类),所以一定程度上可以说Spring的单例Bean是线程安全的。如果你的Bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全。在一般情况下,只有无状态的Bean才可以在多线程环境下共享

大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的(非静态成员变量的写操作)会存在线程安全问题
常见的有两种解决办法:

  • 在Bean对象中尽量避免定义可变的成员变量(不太现实)。
  • 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。

(4.2)循环依赖

如果A对象创建的过程需要使用到B对象,但是B对象创建的时候也需要A对象,也就是构成了循环依赖的现象,那么Spring会如何解决?
答: 这是一种构造器循环依赖,通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖

(5)IOC 中 bean 标签和管理对象细节

(5.1)bean标签

在这里插入图片描述

(5.2)bean的作用范围和生命周期

在这里插入图片描述

(5.3)实例化bean的三种方式

第一种方式:使用默认无参构造函数

<!--在默认情况下:
它会根据默认无参构造函数来创建类对象。如果 bean 中没有默认无参构造函数,将会创建失败。-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"/>

第二种方式:spring管理静态工厂-使用静态工厂的方法创建对象

/**
* 模拟一个静态工厂,创建业务层实现类
*/
public class StaticFactory { 
	public static IAccountService createAccountService(){
		return new AccountServiceImpl();
	}
}

<!-- 此种方式是:
	使用 StaticFactory 类中的静态方法 createAccountService 创建对象,并存入 spring 容器
	id 属性:指定 bean 的 id,用于从容器中获取
	class 属性:指定静态工厂的全限定类名
	factory-method 属性:指定生产对象的静态方法
-->

<bean id="accountService" class="com.itheima.factory.StaticFactory" factory-method="createAccountService"></bean>

第三种方式:spring 管理实例工厂-使用实例工厂的方法创建对象

/**
* 模拟一个实例工厂,创建业务层实现类
* 此工厂创建对象,必须现有工厂实例对象,再调用方法
*/
public class InstanceFactory {
	public IAccountService createAccountService(){
		return new AccountServiceImpl();
	}
}
<!-- 此种方式是:
	先把工厂的创建交给 spring 来管理。
	然后在使用工厂的 bean 来调用里面的方法
	factory-bean 属性:用于指定实例工厂 bean 的 id。
	factory-method 属性:用于指定实例工厂中创建对象的方法。
-->

<bean id="instancFactory" class="com.itheima.factory.InstanceFactory"></bean>
<bean id="accountService" factory-bean="instancFactory" factory-method="createAccountService"></bean>

(6)spring的依赖注入

(6.1)依赖注入的概念

依赖注入:Dependency Injection。它是 spring 框架核心 ioc 的具体实现。
我们的程序在编写时,通过控制反转,把对象的创建交给了 spring,但是代码中不可能出现没有依赖的情况。
ioc 解耦只是降低他们的依赖关系,但不会消除。例如:我们的业务层仍会调用持久层的方法。
那这种业务层和持久层的依赖关系,在使用 spring 之后,就让 spring 来维护了。
简单的说,就是坐等框架把持久层对象传入业务层,而不用我们自己去获取。

(6.2)构造函数注入

顾名思义,就是使用类中的构造函数,给成员变量赋值。注意,赋值的操作不是我们自己做的,而是通过配置的方式,让 spring 框架来为我们注入。具体代码如下:

public class AccountServiceImpl implements IAccountService {
	private String name;
	private Integer age;
	private Date birthday;
	
	public AccountServiceImpl(String name, Integer age, Date birthday) {
		this.name = name;
		this.age = age;
		this.birthday = birthday;
	}
	
	@Override
	public void saveAccount() {
		System.out.println(name+","+age+","+birthday);
	}
}

<!-- 使用构造函数的方式,给 service 中的属性传值
	要求:
		类中需要提供一个对应参数列表的构造函数。
	涉及的标签:
		constructor-arg
			属性:
			index:指定参数在构造函数参数列表的索引位置
			type:指定参数在构造函数中的数据类型
			name:指定参数在构造函数中的名称 用这个找给谁赋值
			=======上面三个都是找给谁赋值,下面两个指的是赋什么值的==============
			value:它能赋的值是基本数据类型和 String 类型
			ref:它能赋的值是其他 bean 类型,也就是说,必须得是在配置文件中配置过的 bean
-->

<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
	<constructor-arg name="name" value="张三"></constructor-arg>
	<constructor-arg name="age" value="18"></constructor-arg>
	<constructor-arg name="birthday" ref="now"></constructor-arg>
</bean>

<bean id="now" class="java.util.Date"></bean>

(6.3)set方法注入

顾名思义,就是在类中提供需要注入成员的 set 方法。具体代码如下:

public class AccountServiceImpl implements IAccountService {
	private String name;
	private Integer age;
	private Date birthday;
	
	public void setName(String name) {
		this.name = name;
	}
	public void setAge(Integer age) {
		this.age = age;
	}
	public void setBirthday(Date birthday) {
		this.birthday = birthday;
	}
	
	@Override
	public void saveAccount() {
		System.out.println(name+","+age+","+birthday);
	}
}


<!-- 通过配置文件给 bean 中的属性传值:使用 set 方法的方式
	涉及的标签:property
		属性:
		name:找的是类中 set 方法后面的部分
		ref:给属性赋值是其他 bean 类型的
		value:给属性赋值是基本数据类型和 string 类型的
	实际开发中,此种方式用的较多。
-->

<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
	<property name="name" value="test"></property>
	<property name="age" value="21"></property>
	<property name="birthday" ref="now"></property>
</bean>

<bean id="now" class="java.util.Date"></bean>

(6.4)使用 p 名称空间注入数据(本质还是调用 set 方法)

此种方式是通过在 xml中导入 p名称空间,使用 p:propertyName 来注入数据,它的本质仍然是调用类中的set 方法实现注入功能。

public class AccountServiceImpl4 implements IAccountService {
	private String name;
	private Integer age;
	private Date birthday;
	
	public void setName(String name) {
		this.name = name;
	}
	public void setAge(Integer age) {
		this.age = age;
	}
	public void setBirthday(Date birthday) {
		this.birthday = birthday;
	}
	
	@Override
	public void saveAccount() {
		System.out.println(name+","+age+","+birthday);
	}
}

配置文件代码:
p名称空间,xmlns:p

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:p="http://www.springframework.org/schema/p"
	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="accountService"
		class="com.itheima.service.impl.AccountServiceImpl4"
		p:name="test" 
		p:age="21" 
		p:birthday-ref="now">
	</bean>
	
</beans>

(6.5)注入集合属性

顾名思义,就是给类中的集合成员传值,它用的也是set方法注入的方式,只不过变量的数据类型都是集合。我们这里介绍注入数组,List,Set,Map,Properties。具体代码如下:

public class AccountServiceImpl implements IAccountService {
	private String[] myStrs;
	private List<String> myList;
	private Set<String> mySet;
	private Map<String,String> myMap;
	private Properties myProps;
	
	public void setMyStrs(String[] myStrs) {
		this.myStrs = myStrs;
	}
	public void setMyList(List<String> myList) {
		this.myList = myList;
	}
	public void setMySet(Set<String> mySet) {
		this.mySet = mySet;
	}
	public void setMyMap(Map<String, String> myMap) {
		this.myMap = myMap;
	}
	public void setMyProps(Properties myProps) {
		this.myProps = myProps;
	}
	
	@Override
	public void saveAccount() {
		System.out.println(Arrays.toString(myStrs));
		System.out.println(myList);
		System.out.println(mySet);
		System.out.println(myMap);
		System.out.println(myProps);
	}
}

<!-- 注入集合数据
	List 结构的:
		array,list,set
	Map 结构的
		map,entry,props,prop
-->

<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
	<!-- 在注入集合数据时,只要结构相同,标签可以互换 -->
	
	<!-- 给数组注入数据 -->
	<property name="myStrs">
		<set>
			<value>AAA</value>
			<value>BBB</value>
			<value>CCC</value>
		</set>
	</property>
	
	<!-- 注入 list 集合数据 -->
	<property name="myList">
		<array>
			<value>AAA</value>
			<value>BBB</value>
			<value>CCC</value>
		</array>
	</property>
	
	<!-- 注入 set 集合数据 -->
	<property name="mySet">
		<list>
			<value>AAA</value>
			<value>BBB</value>
			<value>CCC</value>
		</list>
	</property>
	
	<!-- list结构的都用list -->
	<!-- map结构的都用map -->

	<!-- 注入 Map 数据 -->
	<property name="myMap">
		<props>
			<prop key="testA">aaa</prop>
			<prop key="testB">bbb</prop>
		</props>
	</property>
	
	<!-- 注入 properties 数据 -->
	<property name="myProps">
		<map>
			<entry key="testA" value="aaa"></entry>
			<entry key="testB">
				<value>bbb</value>
			</entry>
		</map>
	</property>
</bean>

(7)Spring的事务

Spring支持编程式事务管理和声明式事务管理两种方式:

  • 编程式事务管理:使用TransactionTemplate实现。
    在代码中硬编码。(不推荐使用)
  • 声明式事务管理:建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务
    在配置文件中配置(推荐使用)
    声明式事务又分为:基于XML的声明式事务;基于注解的声明式事务

声明式事务的优点:
就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中。

事务选择:
声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的开发方式,使业务代码不受污染,只要加上注解就可以获得完全的事务支持。唯一不足之处是声明式事务的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别

(7.1)Spring 事务中的事务传播行为

当多个Spring事务存在的时候,Spring定义了下边的7个传播行为来处理这些事务行为:

支持当前事务的情况:

  • TransactionDefinition.PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
  • TransactionDefinition.PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
  • TransactionDefinition.PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。(mandatory:强制性)

不支持当前事务的情况:

  • TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

其他情况:

  • TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行,等价于TransactionDefinition.PROPAGATION_REQUIRED

(7.2)Spring 事务中的隔离级别

并发操作产生的问题:

  • 丢失更新:AB同时获取相同数据,A提交的事务被B提交的事务覆盖
  • 脏读:A读取了B没有提交的数据,B回滚了
  • 不可重复读:同一次查询数据的值不同
  • 幻读:同一次查询数据的结构(条数)不同

Spring事务的隔离级别和MySQL事务的隔离级别类似,依然存在四种隔离级别。

  • 读未提交 (Read Uncommitted):允许脏读
  • 读已提交 (Read Committed):禁止脏读,允许不可重复读
  • 可重复读 (Repeatable Read):禁止不可重复读和脏读,可能出现幻读
  • 序列化 (Serializable)

TransactionDefinition 接口中定义了五个表示隔离级别的常量:

  • TransactionDefinition.ISOLATION_DEFAULT
    使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ (可重复读)隔离级别
    Oracle 默认采用的 READ_COMMITTED (读已提交) 隔离级别

  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED 读未提交
    最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读

  • TransactionDefinition.ISOLATION_READ_COMMITTED 读已提交
    允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

  • TransactionDefinition.ISOLATION_REPEATABLE_READ 可重复读
    对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

  • TransactionDefinition.ISOLATION_SERIALIZABLE 序列化
    最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

事务ACID理解:
原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)

(7.3)@Transactional(rollbackFor = Exception.class)注解

我们知道:Exception分为运行时异常RuntimeException和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性

当@Transactional注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚

@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚。

关于 @Transactional 注解推荐阅读的文章:
透彻的掌握 Spring 中@transactional 的使用

(8)SpringMVC

SpringMVC是一种轻量级的Web层框架,是一个基于请求驱动的Web框架,使用了“前端控制器(DispatcherServlet)”模型来进行设计,再根据请求映射规则分发给相应的页面控制器进行处理。消息处理依次经过的组件如下:

DispatcherServlet、HandlerMapping、HandlerAdapter,返回一个ModelAndView逻辑视图名、ViewResolver、View

(8.1)Spring MVC 的重要组件说明

1、前端控制器DispatcherServlet(不需要工程师开发),由框架提供(重要)
作用:Spring MVC 的入口函数。接收请求,响应结果,相当于转发器,中央处理器。有了 DispatcherServlet 减少了其它组件之间的耦合度。用户请求到达前端控制器,它就相当于mvc模式中的c,DispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,DispatcherServlet的存在降低了组件之间的耦合性。

2、处理器映射器HandlerMapping(不需要工程师开发),由框架提供
作用:根据请求的url查找Handler。HandlerMapping负责根据用户请求找到Handler即处理器(Controller),SpringMVC提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

3、处理器适配器HandlerAdapter
作用:按照特定规则(HandlerAdapter要求的规则)去执行Handler 通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。

4、处理器Handler(需要工程师开发)
注意:编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。 由于Handler涉及到具体的用户业务请求,所以一般情况需要工程师根据业务需求开发Handler。

5、视图解析器View resolver(不需要工程师开发),由框架提供
作用:进行视图解析,根据逻辑视图名解析成真正的视图(view) View Resolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。 springmvc框架提供了很多的View视图类型,包括:jstlView、freemarkerView、pdfView等。 一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由工程师根据业务需求开发具体的页面。

6、视图View(需要工程师开发)
View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf…)
注意:处理器Handler(也就是我们平常说的Controller控制器)以及视图层view都是需要我们自己手动开发的。其他的一些组件比如:前端控制器DispatcherServlet、处理器映射器HandlerMapping、处理器适配器HandlerAdapter等等都是框架提供给我们的,不需要自己手动开发。

(8.2)Spring MVC 的消息处理流程

具体流程可以概括为:通过前端控制器DispatcherServlet来接收并且分发请求,然后通过HandlerMappingHandlerAdapter找到具体可以处理该请求的Handler,经过逻辑处理,返回一个ModelAndView,经过ViewResolver处理,最后生成了一个View视图返回给了客户端。可以参考如下的示意图:
在这里插入图片描述

流程说明(重要):

  • 客户端(浏览器)发送请求,直接请求到 DispatcherServlet
  • DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler
  • 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。
  • HandlerAdapter 会根据 Handler调用真正的处理器开处理请求,并处理相应的业务逻辑
  • 处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View
  • ViewResolver 会根据逻辑 View 查找实际的 View
  • DispaterServlet 把返回的 Model 传给 View(视图渲染)。
  • View 返回给请求者(浏览器)

(9)Spring 框架中的设计模式

参考文章:
面试官:“谈谈Spring中都用到了那些设计模式?”

(9.1) 工厂设计模式

Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。

(9.2) 代理设计模式

Spring AOP 功能的实现。

(9.3) 单例设计模式

Spring 中的 Bean 默认都是单例的。

在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果

使用单例模式的好处:
1、对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
2、由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

Spring 实现单例的方式:

xml : <bean id="userService" class="top.snailclimb.UserService" scope="singleton"/>
注解:@Scope(value = "singleton")

Spring 通过 ConcurrentHashMap (线程安全)实现单例注册表的特殊方式实现单例模式

(9.4) 模板方法模式

模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式

Spring 中 jdbcTemplatehibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。

一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性

(9.5) 包装器 / 装饰者设计模式

我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。

装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个Decorator套在原有代码外面。其实在 JDK 中就有很多地方用到了装饰者模式,比如 InputStream家族,InputStream 类下有 FileInputStream (读取文件)、BufferedInputStream (增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream 代码的情况下扩展了它的功能

在这里插入图片描述

Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源。我们能否根据客户的需求在少修改原有类的代码下动态切换不同的数据源?这个时候就要用到装饰者模式(这一点我自己还没太理解具体原理)。Spring 中用到的包装器模式在类名上含有 Wrapper或者 Decorator。这些类基本上都是动态地给一个对象添加一些额外的职责

(9.6) 观察者模式

观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。

① Spring 事件驱动模型中的三种角色

事件角色

ApplicationEvent (org.springframework.context包下)充当事件的角色,这是一个抽象类,它继承了java.util.EventObject并实现了 java.io.Serializable接口。

Spring 中默认存在以下事件,他们都是对 ApplicationContextEvent 的实现(继承自ApplicationContextEvent):

  • ContextStartedEvent:ApplicationContext 启动后触发的事件;
  • ContextStoppedEvent:ApplicationContext 停止后触发的事件;
  • ContextRefreshedEvent:ApplicationContext 初始化或刷新完成后触发的事件;
  • ContextClosedEvent:ApplicationContext 关闭后触发的事件。

在这里插入图片描述

事件监听者角色

ApplicationListener 充当了事件监听者角色,它是一个接口,里面只定义了一个 onApplicationEvent()方法来处理ApplicationEvent。ApplicationListener接口类源码如下,可以看出接口定义看出接口中的事件只要实现了 ApplicationEvent就可以了。所以,在 Spring中我们只要实现 ApplicationListener 接口实现 onApplicationEvent() 方法即可完成监听事件

package org.springframework.context;
import java.util.EventListener;
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E var1);
}
事件发布者角色

ApplicationEventPublisher 充当了事件的发布者,它也是一个接口

@FunctionalInterface
public interface ApplicationEventPublisher {
    default void publishEvent(ApplicationEvent event) {
        this.publishEvent((Object)event);
    }

    void publishEvent(Object var1);
}

ApplicationEventPublisher 接口的publishEvent()这个方法在AbstractApplicationContext类中被实现,阅读这个方法的实现,你会发现实际上事件真正是通过ApplicationEventMulticaster来广播出去的。具体内容过多,就不在这里分析了。

②Spring 的事件流程总结

  1. 定义一个事件: 实现一个继承自 ApplicationEvent,并且写相应的构造函数;
  2. 定义一个事件监听者:实现 ApplicationListener 接口,重写 onApplicationEvent() 方法;
  3. 使用事件发布者发布消息: 可以通过 ApplicationEventPublisherpublishEvent() 方法发布消息。
// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数
public class DemoEvent extends ApplicationEvent{
    private static final long serialVersionUID = 1L;

    private String message;

    public DemoEvent(Object source,String message){
        super(source);
        this.message = message;
    }

    public String getMessage() {
         return message;
	}
}

// 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法;
@Component
public class DemoListener implements ApplicationListener<DemoEvent>{

    //使用onApplicationEvent接收消息
    @Override
    public void onApplicationEvent(DemoEvent event) {
        String msg = event.getMessage();
        System.out.println("接收到的信息是:"+msg);
    }
}

// 发布事件,可以通过ApplicationEventPublisher  的 publishEvent() 方法发布消息。
@Component
public class DemoPublisher {

    @Autowired
    ApplicationContext applicationContext;

    public void publish(String message){
        //发布事件
        applicationContext.publishEvent(new DemoEvent(this, message));
    }
}

(9.7) 适配器模式

Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。

适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)

① spring AOP中的适配器模式

我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter

Advice 常用的类型有:BeforeAdvice(目标方法调用前,前置通知)、AfterAdvice(目标方法调用后,后置通知)、AfterReturningAdvice(目标方法执行结束后,return之前)等等。每个类型Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptorAfterReturningAdviceAdapterAfterReturningAdviceInterceptor

Spring预定义的通知要通过对应的适配器,适配成 MethodInterceptor接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor 负责适配 MethodBeforeAdvice)。

② spring MVC中的适配器模式

在Spring MVC中,DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。

为什么要在 Spring MVC 中使用适配器模式?
Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样:

if(mappedHandler.getHandler() instanceof MultiActionController){  
   ((MultiActionController)mappedHandler.getHandler()).xxx  
}else if(mappedHandler.getHandler() instanceof XXX){  
    ...  
}else if(...){  
   ...  
}  

假如我们再增加一个 Controller 类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭

(10)SpringBoot

  • Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring
    的难度,简省了繁重的配置,提供了各种启动器starter,开发者能快速上手。
  • SpringBoot的优点包括可以独立运行,简化了配置,可以实现自动配置,无代码生成以及XML配置,并且可以进行应用监控。
  • 注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,实现了SpringBoot项目的自动配置。

(10.1)SpringBoot的核心注解

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

  • @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项。
    如关闭数据源自动配置功能:@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})。
  • @ComponentScan:Spring组件扫描。

(10.2)SpringBoot项目启动分析

Application类是通过SpringApplication类的静态run方法来启动应用的。打开这个静态方法,该静态方法真正执行的是两部分:new SpringApplication( )并且执行对象run()方法

(10.3)自动配置的实现原理

参考博文:
Spring Boot面试杀手锏————自动配置原理

Spring Boot关于自动配置的源码在spring-boot-autoconfigure-x.x.x.x.jar中:

Spring Boot的启动类上有一个@SpringBootApplication注解,这个注解是一个派生注解,在@SpringBootApplication中有一个注解@EnableAutoConfiguration意思就是开启自动配置。

@EnableAutoConfiguration关键功能由@Import注解提供,其导入的AutoConfigurationImportSelectorselectImports()方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories的jar包。

spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件。

这个spring.factories文件也是一组一组的key=value的形式,其中一个keyEnableAutoConfiguration类的全类名,而它的value是一个xxxxAutoConfiguration的类名的列表,这些类名以逗号分隔。

这个@EnableAutoConfiguration注解通过@SpringBootApplication被间接的标记在了Spring Boot的启动类上。在SpringApplication.run(...)的内部就会执行selectImports()方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。

(10.4)项目的启动流程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值