美文网首页
Spring Framework 学习笔记(2) Spring

Spring Framework 学习笔记(2) Spring

作者: 张云飞Vir | 来源:发表于2021-07-06 08:28 被阅读0次

    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 每次都创建一个新的实例
    request Web应用的一次请求期间
    session Web应用的会话期间
    application Web应用期间
    websocket websocket 范围

    使用 @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

    相关文章

      网友评论

          本文标题:Spring Framework 学习笔记(2) Spring

          本文链接:https://www.haomeiwen.com/subject/qlvjultx.html