Spring Framework 学习笔记(2) Spring Core 核心

1. 背景

Spring 是为了简化企业级开发而创建的,在 Spring 框架全家桶中绝对是不可或缺技术。

2.基础概念

为了降低Java开发的复杂性,Spring 采用了以下四种关键策略:

  • 基于POJO( Java Ben ) 轻量级和最小侵入性编程。
  • 通过依赖注入和和面向接口实现松耦合。
  • 基于切面和惯例进行声明式编程。
  • 基于切面和样板减少样板代码。

Spring 所做的事情都是围绕这几点展开。

依赖注入
依赖注入( Dependency Injection , DI ) 听起来让人生畏,实际上并没有听上去那么复杂。

IoC (Inversion of Control,缩写为IoC)也称为依赖注入 ( Dependency Injection , DI )。是指“一个对象被创建时,先定义其构造方法的参数或者工厂方法的参数(即其使用的对象),然后容器在创建 bean 时注入这些依赖项的过程”。

对比区别:

  • 传统方法是:Class A中用到了Class B,需要在A的代码中new一个B的对象。
  • 依赖注入是:定义好A和B,用XML描述A依赖B的关系,在容器容器创建A时,将B对象注入到A的示例对象中。通过容器创建出来就可以直接使用了,无需再New 一个。

面向切面编程 ( AOP )

AOP ( Aspect Oriented Programming ) ,面向切面编程。其中的 Aspect 指 切面,中文的意思可理解为“维度”。

AOP是“关注点分离”的一项技术,软件系统往往由多个组件/模块组成,每个组件各负责一块特定的功能。这些组件往往还承担额外的职责,比如日志,事务,安全控制等系统服务逻辑,和业务功能混合在一起。这些系统服务逻辑会在多个组件/模块中存在,被称为“横切关注点”。

这些模块中调用的系统服务逻辑分散到多个组件/模块中去,导致你需要维护多个组件的代码,带来复杂性。即使把这些关注点抽离成一个独立的模块,但方法的调用还是出现在各个模块中。

而AOP可以使得这些关注点切面模块化,以声明的方式应用到具体业务组件/模块中去,使得这些业务模块更加内聚和更加关注自身的业务。

AOP

使用模块消除 “ 样板代码 ”
样板代码是指重复的代码,比如 传统JDBC 中要开启数据库连接,构造预处理语句等,每次都要写很多。借助使用 模板 Template 封装可以帮助消除样板代码,简化复杂性,模板 使得你的代码更关注与自身的业务职责。

Spring 容器,依赖注入( Dependency Injection , DI ),和面向切面编程( Aspect-Orientd Programming, AOP ) 是 Spring 框架的核心。下面分别介绍。

3. 容器 ( ApplicationContext )

3.1 容器的介绍

org.springframework.context.ApplicationContext 接口代表 Spring IoC 容器,负责实例化、配置和组装 bean。

  • 容器通过读取 “配置元数据” 来获取如何创建和装配对象。
  • “配置元数据” 可以是 XML配置文件,Java注解,或者Java代码来表示。

ApplicationContext 基于 BeanFactory 构建,BeanFactory 提供了配置框架和基本功能, 而 ApplicationContext 添加了更多企业特定的功能。我们更多使用的是 ApplicationContext 。

Spring 提供了几种 ApplicationContext 实现

  • ClassPathXmlApplicationContext 从类路径下加载 XML 配置文件
  • FileSystemXmlApplicationContext 从文件系统 加载 XML 配置文件
  • AnnotationConfigApplicationContext 基于 注解 的上下文,从注解加载

示例:

ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

3.2 Bean 的生命周期

有了容器,容器负责创建和管理Bean,还要进一步了解下 Bean 的生命周期:

Bean 声明周期

3.3 代码示例

依赖类库
以 Maven 方式时,添加 spring-context 依赖。

<dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.4</version>
        </dependency>
    </dependencies>

Spring 支持多种方式加载配置

  • (1) XML 方式
  • (2) Java 方式

XML 使用 ClassPathXmlApplicationContext 或者 FileSystemXmlApplicationContext 从一个 XML 文件中初始化容器对象。

示例:
xml 文件

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

    <bean id="hero" class="cn.zyfvir.demo.Hero">
        <constructor-arg ref="swordAction"/>
    </bean>

    <bean id="swordAction" class="cn.zyfvir.demo.SwordAction">
        <constructor-arg name="printStream" value="#{T(System).out}"/>
    </bean>

</beans>
// 使用 xml 方式 配置 spring
    private static void demoXmlSpring() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        Hero bean = context.getBean(Hero.class);
        bean.play();
    }

AnnotationConfigApplicationContext 上下文支持从注解和Java代码方式配置对象。

示例:

@Configuration
public class HeroConfig {

    @Bean
    public Hero hero() {
        return new Hero(action());
    }

    @Bean
    public Action action() {
        return new SwordAction(System.out);
    }
}
// 使用java 方式
    private static void demoJavaSpring() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        // 启用组件扫描,扫描查找任何带 @Component ,@Configuration 注解的类
        context.scan("cn.zyfvir.demo");
        context.refresh();
        Hero bean = context.getBean(Hero.class);
        bean.play();
    }

我的 demo 示例代码见:https://github.com/vir56k/java_demo/tree/master/spring_demo1

3. 依赖注入 DI ( 装配Bean )

3.1 装配( Wiring )

装配( Wiring ): 在 Spring 中,对象无需自己查找和创建与其关联的其他对象。由 容器 负责把需要协作的各个对象赋予对象。创建对象之间协作关系的行为成为 装配( Wiring )。这也是依赖注入 DI 的本质

Spring 提供了三种 Bean 的装配方式:

  • 在XML中配置
  • 通过 Java 方式配置
  • 自动装配

怎么选择呢?一些建议是:

  • 尽量使用 自动装配 的方式,使用起来比较省事,它不用显示的针对 每个Bean 的依赖关系配置。
  • 其次,使用Java 方式配置,它是类型安全的,比 XML 更强大直观。
  • 最后才选择 XML 方式。

3.2 自动装配 Bean

如果 Spring 能自己装配的话,何必再用 XML 等方式具体声明呢?自动装配能带来很多的便利。

Spring 从两个角度实现自动装配:

  • 组件扫描 ( Component Scanning ) :Spring 会自动扫描和发现需要创建的Bean
  • 自动装配 ( autowiring ):Spring 自动满足 Bean 之间的依赖

@Component 注解可以作用于一个 类上,用于声明一个bean对象。
@ComponentScan 注解用于启用组件扫描。默认会扫描与其处于相同包下的类。它也可以通过 basePackages属性指定具体包。
@Autowired 注解声明了自动装配,Spring 会选择匹配合适的Bean来装配。它可以作用在构造方法和set方法上。

3.3 通过Java 代码配置

@Configuration 声明了这个类是个配置类,它不是必须的。
通过 @Bean 注解声明这个方法返回一个对象,这个对象要注册到 Spring 的上下文中。

比如:

@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}

3.4 通过XML装配

通过<bean> 标签描述。

  • id 属性是个 bean 的标识符
  • class 属性定义 bean 的类型并使用完全限定的类名
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="..." class="...">  </bean>

    <bean id="myService" class="com.acme.services.MyServiceImpl"/>

</beans>

3.5 混合使用

使用 @Import , 或者 @ImportResource 等注解。
详细参考:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-introduction

3.6 代码示例

下面编写一个自动装配的示例:

/**
 * @description: 光盘
 * @author: zhangyunfei
 * @date: 2021/7/3 22:22
 */
public interface CompactDisc {

    void play();

}

/**
 * @description: VCD 光盘
 * @author: zhangyunfei
 * @date: 2021/7/3 22:25
 */
@Component
public class VcdCompactDisc implements CompactDisc {
    private String title = "经典歌曲-忘情水";

    public void play() {
        System.out.println(String.format(" %s  正在播放...", title));
    }

}

在使用时,关键在于 Autowired 的注解,它会自动寻找到合适的对象注入到这里。

/**
 * @description: 播放器
 * @author: zhangyunfei
 * @date: 2021/7/3 22:16
 */
@Component
public class Player {
    private CompactDisc compactDisc;

    // 自动装配
    @Autowired
    void insertCompactDisc(CompactDisc action) {
        this.compactDisc = action;
    }

    /**
     * 开始播放
     */
    void startPlay() {
        compactDisc.play();
    }

}

还要配置“ 自动扫描要装配的组件 ”, ComponentScan 用于声明搜索当前的包,我这里是个空的类。

@Configuration
@ComponentScan
public class PlayerConfig {
}

main 方法演示如何调用:

public class MainClass {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PlayerConfig.class);
        Player bean = context.getBean(Player.class);
        bean.startPlay();
    }
}

我的代码示例见:https://github.com/vir56k/java_demo/tree/master/spring_demo2_autowired

4. 面向切面 ( AOP )

AOP ( Aspect Oriented Programming, AOP ) 面向切面编程,是指在编译时期方式或者运行时期动态代理的方式,将代码织入到指定位置(具体的类的方法)上的一种编程思想,就是面向切面的编程。

为什么要用AOP
具体要看场景和时机,比如下图:

AOP

类似于 日志,事务这样的功能要想模块化的话面临一些选择,比如对象继承和委托。继承的话整个 应用中都有同样的基类,往往导致一个脆弱的对象体系,而委托可能需要对委托对象进行复杂的调用。

在各个业务模块挨个写调用也太麻烦了,不利于维护。而 切面是一个可供选择方案,使用AOP可以以声明的方式的方式在外部应用,而不用修改(影响)到具体的业务功能模块。这也是 “关注点分离”的体现,每个关注点都集中于一个地方,而不是分散到各处的代码。

多种AOP实现
AOP 是一种编程范式,可以有多种方式实现:

  • 代理方式,比如 Spring AOP
  • 编译时方式,比如 AspectJ
  • 类加载期

代理方式分为静态代理和动态代理,静态代理可理解为自己写的代理或者字节码方式的代理。动态代理是在运行时生成一个代理类(实现类)再由其去访问目标对象的方式。

Spring AOP

Spring AOP 是通过 动态代理 的方式实现的AOP

  • 如果要代理的对象,声明其实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象。创建代理类和新的目标代理实现,调用时通过代理类访问 目标代理实现。
  • 没有接口声明时,Spring AOP会使用Cglib,生成一个被代理对象的子类,来作为代理。

AspectJ 是编译成class时织入的,拥有更强大的能力。

类加载期是在目标类加载到JVM时织入,需要特殊的类加载器,比如 AspectJ 5 的load-time weaving,LTW 就支持这种方式。

AOP 术语:

  • advise 通知:接收的消息(通知),在什么时机被得知。
  • pointcut 切点:描述了在哪里切,比如某个 名字的方法。
  • join point 连接点:切落在那个点上,比如 3个叫做 getSome 的具体方法上。

Spring AOP 通知的类型:

  • 前置通知(Before): 在目标方法被调用 "前" 的通知。
  • 后置通知(After): 在被调用 "后" 的通知。
  • 返回通知(After-returning): 在 "成功" 执行后的通知。
  • 异常通知(After-throwing): 在 "抛出异常" 后的通知。
  • 环绕通知(Around): 将方法 "完全包裹" 的通知,可以获得目标方法执行,因而可以调用前后等自定义时机,甚至多用多次来实现重试机制。

代码示例
(1) 引用类库

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.3.4</version>
        </dependency>

(2) 声明 启用 Aspect 的自动代理配置

// 启用:组件搜索
// 启用:aspect 的自动代理
@Component
@ComponentScan
@EnableAspectJAutoProxy
public class MySrpingConfig {
}

编写AOP通知的切入点

// 博客服务
interface BlogService {
    public void postBlog(String blogContet);
}

@Component
class BlogServiceImpl implements BlogService {

    public void postBlog(String blogContet) {
        System.out.println("发布了一遍博客");
    }

}


// 日志记录员
// 把自己也注册成 Spring 组件
@Aspect
@Component
class LogAspect {

    // 切点表达式
    @Pointcut("execution(* cn.zyfvir.demo.BlogService.postBlog(..))")
    public void doPostPoint() {
    }

    @Before("doPostPoint()")
    public void before() {
        System.out.println("## before...");
    }

    @After("doPostPoint()")
    public void after() {
        System.out.println("## after...");
    }

    @AfterReturning("doPostPoint()")
    public void afterReturning() {
        System.out.println("## AfterReturning...");
    }

    @AfterThrowing("doPostPoint()")
    public void afterThrowing() {
        System.out.println("## AfterThrowing...");
    }
}

通过AOP,达到了对实际的业务调用无影响,正常使用即可,示例:

public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MySrpingConfig.class);
        BlogService bean = context.getBean(BlogService.class);
        bean.postBlog("《从入门到精通》");
        System.out.println("执行结束," + bean);
    }

而在执行过程中,日志记录类会被触发和工作。
我的代码示例见:https://github.com/vir56k/java_demo/tree/master/spring_demo5_aop

5. 扩展:高级装配

5.1 环境与 @Profile 注解

在实际开发中经常会有多个环境,比如 dev 开发环境,test 测试环境,product 正式环境。Spring 对环境做了一层抽象,允许你定义多个环境,和激活使用的某个环境。

关键点是:

  • 声明一个环境, 和在环境下才被使用的对象
  • 激活一个环境

使用 @Profile 可以声明某个bean只在某个环境下可用(被激活)。比如下面的示例,它使用 @Profile 的注解来声明了 这个类 只有在 development 环境下才可用。

@Configuration
@Profile("development")
public class StandaloneDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}

激活 配置的环境,可以使用代码的方式,比如:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

也可以在 以java 命令行启动时指定:
使用 环境变量 “ spring.profiles.active ” 来声明激活的环境,如以下示例所示:

-Dspring.profiles.active="profile1,profile2"

5.2 有条件的选择 Bean 与 @Conditional 注解

实际上面说的 @Profile 也是通过 @Conditional 注解来实现的。
@Conditional 注解可用于指示在特定情况下才 注册某个 Bean。

示例:

@Configuration
public class PersonConfig {

    @Bean()
    @Conditional({ConditionalDemo1.class})
    public Person person1(){
        return new Person("Bill Gates",62);
    }
}

上面的示例使用了 @Conditional 注解,它指定了参数ConditionalDemo1.class 。@Conditional 的参数实际是这么一个接口,你可以根据你的需要来实现:

public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

比如,@Profile 注解实际是这么实现的:

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    // Read the @Profile annotation attributes
    MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
    if (attrs != null) {
        for (Object value : attrs.get("value")) {
            if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                return true;
            }
        }
        return false;
    }
    return true;
}

5.3 处理自动装配时的歧义

在自动装配时,如果有多个可被选中的对象无法被确定时,就出现异常了。
比如遇到下面的情形:

// 甜点
interface Dessert {
    String getName();
}

@Component
class IceCream implements Dessert{
    private String name = "冰淇淋";

    public String getName() {
        return name;
    }

}

@Component
class Chocolate implements Dessert{
    private String name = "巧克力";

    public String getName() {
        return name;
    }
}

@Component
class Lollipop implements Dessert{
    private String name = "棒棒糖";

    public String getName() {
        return name;
    }
}

上面的示例实现了多个 甜点 类,小朋友不知吃哪个了。

@Component
public class Child {
    Dessert dessert;

    // 想要
    @Autowired
    public void wantDessert(Dessert dessert) {
        this.dessert = dessert;
    }

    public void eating() {
        System.out.println(String.format("正在吃 %s ...", dessert.getName()));
    }

}

这时,可以选择处理歧义的方式:

  • 通过 @Primary 声明一个 “优先被选择的”
  • 通过 @Qualifier 限定名注解,指定一个 Bean 名称。

示例:

// 想要
    @Autowired
    @Qualifier("lollipop") // @Qualifier 声明了一个 优先选择的 限定名。
    public void wantDessert(Dessert dessert) {
        this.dessert = dessert;
    }

我的代码示例见:https://github.com/vir56k/java_demo/tree/master/spring_demo3

5.4 Bean 作用域 Scope

默认情况下,Spring 创建的实例 是都单例的,即 singleton。

Sping 支持多种 作用域(Scope),包括:

Scope描述
singleton单个实例
prototype每次都创建一个新的实例
requestWeb应用的一次请求期间
sessionWeb应用的会话期间
applicationWeb应用期间
websocketwebsocket 范围

使用 @Scope 注解可以为一个 Bean 指定 Scope,示例:

@Scope("prototype")
@Component
class IceCream implements Dessert{
    ...
}

5.5 运行时装配

运行时装配的场景,比如动态获取 配置文件中的内容,或者 某个方法的执行结果,或者一个随机数,或者某个 表达式结果。

  • 使用 @PropertySource 注解可以读取配置文件
  • 使用 @Value 注解,可以获取外部的属性值
  • 在 Value 注解中可以使用 ${ ... } 这样的表达式读取值
    示例如下:
@Configuration
@PropertySource("classpath:myproperty_config.properties")
public class MyPropertyConfig {

    // 读取 配置文件中的 author.name
    @Value("${author.name}")
    public String authorName;

}

5.6 SpEL

SpEL 是指 Spring 表达式语言( Spring Expression Language , SpEL ),它能够以简洁和强大的方式将值装配到Bean的属性中,使用表达式会在运行时计算得到值。

SpEL 特性:

  • 引用 Bean
  • 调用方法或者访问属性
  • 算数运算,关系运算,逻辑运算
  • 正则表达式
  • 集合操作

SpEL 的格式: #{ .. }
它以 # 开头。

示例:

设置默认值
    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;

@Configuration
@PropertySource("classpath:myproperty_config.properties")
public class MyPropertyConfig {

    // 读取 配置文件中的 author.name
    @Value("${author.name}")
    public String authorName;

    @Value("#{3.1415}")
    public String pi;

    @Value("#{'xxxxx'}")
    public String string1;

    @Value("#{myPropertyConfig.getMyName().toUpperCase()}")
    public String myName;

    @Value("#{T(java.lang.Math).PI}")
    public String PI;

    @Value("#{T(java.lang.Math).random()}")
    public String random;

    @Value("#{ myPropertyConfig.pi == 3.14 }")
    public boolean is3_14;

    @Value("#{ myPropertyConfig.pi ?:'333' }")
    public String stirng2;

    public String getMyName() {
        return "zhang3";
    }

    @Value("#{ myPropertyConfig.getArray() }")
    public String[] array;


    @Value("#{ myPropertyConfig.array[1] }")
    public String array1;

    public String[] getArray() {
        String[] arr = new String[3];
        arr[0] = "#1";
        arr[1] = "#2";
        arr[2] = "#3";
        return arr;
    }

}

6.参考

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans

https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Artifacts

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值