Spring 讲解(七)

00、Spring 简史

  • 石器时代

    Spring 1.x 时代,一个项目看上去全都是 xml 文件,里面配置着各种各样的 bean,项目越大,xml 文件就越多,到最后人都感觉不好了,像回到了原始社会。

  • 黑铁时代

    Spring 2.x 时代,终于可以使用注解配置 bean 了,这主要得益于 JDK 1.5 新增的注解功能。一个小小的注解,比如 @Component@Service 就可以替代一大段的 xml 配置代码,简直爽的不要不要的。

  • 黄金时代

    Spring 3.x 时代至今,可以使用 Java 配置的方式了。那什么是 Java 配置呢?

    其实从形式上看,Java 配置和注解配置没什么区别,因为 Java 配置也用的是注解,只不过,以前用 application-context.xml 配置数据源等信息,现在用 @Configuration 注解的类配置。

01、Spring 丰富的特性和生态

  • Spring 非常丰富的特性

    1)核心技术:依赖注入(DI)、面向切面编程(AOP)、国际化、数据绑定、类型转换

    2)测试:TestContext 框架、Spring MVC 测试

    3)数据访问:事务、JDBC、对象关系映射(ORM)

    4)Spring MVC(一个模型 - 视图 - 控制器「MVC」的 Web 框架)、Spring WebFlux(一个典型非阻塞异步的框架)

    5)Spring Integration(面向企业应用集成)

  • Spring 生态

    1)Spring Boot:当下最火的一个 Spring 项目了,使用它我们可以快速搭建好一个项目

    2)Spring Cloud:为分布式开发提供了强大的工具集

    3)Spring Security:通过认真和授权保护应用

    4)Spring Web Flow:基于 Spring MVC 的流程式 Web 应用

    5)等等等等

02、快速搭建 Spring 项目

  • 推荐使用 Intellij IDEA 作为集成开发工具

    1)打开 IDEA,依次点击 File→New→Project→Maven。

    2)点击 Next 后填写项目的名称,然后创建项目。打开 pom.xml 文件,添加以下内容。

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.2.RELEASE</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
    dependencies 节点下添加了 Spring 的核心依赖 spring-context,它为 Spring 提供了一个运行时的环境,用以保存各个对象的状态。它依赖于 Spring 的四个核心组件:`spring-aop、spring-beans、spring-core、spring-expression`,也就是说只要引入 spring-context 的依赖,Maven 会自动引入这四个核心。
    
    build 节点下添加了 Maven 的编译插件,并且指定了 Java 的编译版本为 1.8。
    

    3)IDEA 下编辑文件会自动保存,所以与此同时,Maven 会将 Spring 的依赖包全部下载到本地仓库中(Maven建议配置国内的阿里云镜像)。等依赖下载完毕就可以看到 Spring 的依赖包了。

    4)Maven 是一个软件项目管理工具,基于项目对象模型(Project Object Model,明白 pom.xml 的由来了吧?)的概念,可以管理项目依赖的 jar 包,对项目进行编译打包等。

    5)新建 HelloService 类,代码如下:

    @Service
    public class HelloService {
        public void hello (String what) {
            System.out.println("hello " + what);
        }
    }
    
    @Service 注解一般在业务逻辑层使用。该类非常简单,只有一个方法 hello(),参数是字符串,然后在控制台打印 hello xxx。
    

    6)新建 HelloConfig 类,代码如下:

    @Configuration
    public class HelloConfig {
        @Bean
        public HelloService helloService() {
            return new HelloService();
        }
    }
    
    @Configuration 注解表明当前类是一个配置类,相当于 Spring 配置的一个 xml 文件。
    
    @Bean 注解用在 helloService() 方法上,表明当前方法返回一个 Bean 对象(HelloService),然后将其交给 Spring 管理。产生这个 Bean 对象的方法只会被调用一次,随后 Spring 将其放在自己的 IOC 容器中。
    

    7)新建 HelloMain 类,代码如下:

    public class MainMethod {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = 
                new AnnotationConfigApplicationContext(HelloConfig.class);
            HelloService helloService = context.getBean(HelloService.class);
            helloService.hello("沉默王二");
            context.close();
        }
    }
    
    MainMethod 类中有一个 main() 方法,它负责将当前项目跑起来。
    
    AnnotationConfigApplicationContext 是一个用来管理注解 Bean 的容器,可以将 @Configuration 注解的类 Class 作为参数获取容器对象。再通过 getBean() 方法获取注册的 Bean 对象。获取到 HelloService 对象后,就可以让它在控制台打印:"hello 沉默王二"。
    

04、Spring Bean 的配置

  • Bean 的 Scope 配置:singleton

    singleton:单例模式,如果把一个 Bean 的 Scope 定义为 singleton,意味着一个 Bean 在 Spring 容器中只会创建一次实例,对该实例的任何修改都会反映到它的引用上面。这也是 Scope 的默认配置项,可省略。

    新建一个 Writer 、SingletonConfig、SingletonMain 类,内容如下:

    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public class Writer {
        private String name;
    }
    ============================================================================
    @Configuration
    public class SingletonConfig {
        @Bean
        // @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
        // @Scope("singleton")
        // 二者任选其一即可,也可以不写,默认是singleton
        public Writer getWriterSingleton() {
            return new Writer();
        }
    }
    
    @Configuration 注解表明当前类是一个配置类,相当于 Spring 配置的一个 xml 文件。
    @Bean 注解用在 getWriterSingleton() 方法上,表明当前方法返回一个 Bean 对象(Writer),然后将其交给 Spring 管理。
    
    可以使用 Spring 定义的常量来代替字符串 singleton:
    @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)、@Scope("singleton")
    二者任选其一即可,也可以不写,默认是singleton,于是 SingletonConfig 瘦身了。
       ============================================================================
    public class SingletonMain {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = 
                new AnnotationConfigApplicationContext(SingletonConfig.class);
            Writer writer1 = context.getBean(Writer.class);
            Writer writer2 = context.getBean(Writer.class);
    
            System.out.println(writer1);
            System.out.println(writer2);
    
            writer1.setName("沉默王二");
            System.out.println(writer2.getName());
    
            context.close();
        }
    }
    ============================================================================
    控制台打印如下:
    com.rookie.bean.Writer@221af3c0
    com.rookie.bean.Writer@221af3c0
    沉默王二
    

    writer1 和 writer2 两个对象的字符串表示形式完全一样,都是 com.rookie.bean.Writer@221af3c0;另外,改变了 writer1 对象的 name,writer2 也跟着变了。

    从结果中我们可以得出这样的结论:Scope 为 singleton 的时候,尽管使用 getBean() 获取了两次 Writer 实例,但它们是同一个对象。只要更改它们其中任意一个对象的状态,另外一个也会同时改变。

  • Bean 的 Scope 配置:prototype

    prototype 的英文词义是复数的意思,它表示一个 Bean 会在 Spring 中创建多次实例,适合用于多线程的场景。

    新建一个 PrototypeConfig 、PrototypeMain 类,内容如下:

    @Configuration
    public class PrototypeConfig {
        @Bean
        @Scope("prototype")
        public Writer getWriterPrototype() {
            return new Writer();
        }
    }
    可以使用 Spring 定义的常量来代替字符串 prototype:
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ============================================================================
    public class PrototypeMain {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = 
                new AnnotationConfigApplicationContext(PrototypeConfig.class);
            Writer writer1 = context.getBean(Writer.class);
            Writer writer2 = context.getBean(Writer.class);
    
            System.out.println(writer1);
            System.out.println(writer2);
    
            writer1.setName("沉默王二");
            System.out.println(writer2.getName());
    
            context.close();
        }
    }
    ============================================================================
    控制台打印如下:
    com.rookie.bean.Writer@dd3b207
    com.rookie.bean.Writer@551bdc27
    null   
    

    writer1 和 writer2 两个对象的字符串表示形式完全不一样,一个是 com.rookie.bean.Writer@dd3b207,另一个是 com.rookie.bean.Writer@551bdc27;另外,虽然 writer1 对象的 name 被改变为“沉默王二”,但 writer2 的 name 仍然为 null。

    从结果中我们可以得出这样的结论:Scope 为 prototype 的时候,每次调用 getBean() 都会返回一个新的实例,它们不是同一个对象。更改它们其中任意一个对象的状态,另外一个并不会同时改变。

  • request、session、application、websocket

    这 4 个作用域仅在 Web 应用程序的上下文中可用,在实践中并不常用。request 用于为 HTTP 请求创建 Bean 实例,session 用于为 HTTP 会话创建 Bean 实例, application 用于为 ServletContext 创建 Bean 实例,而 websocket 用于为特定的 WebSocket 会话创建 Bean 实例。

05、Bean 的字段注入

  • 注入普通字符串

    新建一个 ValueConfig 类、 ValueMain 类

    @Configuration
    public class ValueConfig {
        @Value("沉默王二")
        private String name;
    
        public void output() {
            System.out.println(name);
        }
    }
    @Value 注解用在成员变量 name 上,表明当前注入 name 的值为“沉默王二”。
    ============================================================================
    public class ValueMain {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = 
            new AnnotationConfigApplicationContext(ValueConfig.class);
    
            ValueConfig service = context.getBean(ValueConfig.class);
            service.output();
    
            context.close();
        }
    }
    ============================================================================
    控制台打印如下:
    沉默王二
    
  • 注入 Spring 表达式

    使用 @Value 注入普通字符串的方式最为简单,还可以注入 Spring 表达式。

    新建一个 ValueConfig 类、 ValueMain 类

    @Configuration
    public class ValueConfig {
    
        @Value("#{18 + 12}") // 30
        private int add;
    
        @Value("#{1 == 1}") // true
        private boolean equal;
    
        @Value("#{400 > 300 || 150 < 100}") // true
        private boolean or;
    
        @Value("#{2 > 1 ? '沉默是金' : '不再沉默'}") // "沉默是金"
        private String ternary;
    
        public void outAdd() {
            System.out.println(add);
        }
    
        public void outEqual() {
            System.out.println(equal);
        }
    
        public void outOr() {
            System.out.println(or);
        }
    
        public void outTernary() {
            System.out.println(ternary);
        }
    }
    ============================================================================
    public class ValueMain {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = 
                new AnnotationConfigApplicationContext(ValueConfig.class);
    
            ValueConfig service = context.getBean(ValueConfig.class);
            service.outAdd();
            service.outEqual();
            service.outOr();
            service.outTernary();
    
            context.close();
        }
    }
    ============================================================================
    控制台打印如下:
    30
    true
    true
    沉默是金
    
  • 注入配置文件

    在 resources 目录下新建 value.properties 文件,内容如下:

    name=沉默王二

    age=18

    新建 ValuePropertiesConfig、ValuePropertiesMain 类,内容如下:

    @Configuration
    @PropertySource("classpath:value.properties")
    public class ValuePropertiesConfig {
    
        @Value("${name}")
        private String name;
    
        @Value("${age}")
        private int age;
    
        public void output() {
            System.out.println("姓名:" + name + " 年纪:" + age);
        }
    }
    @PropertySource 注解用于指定载入哪个配置文件(value.properties),classpath: 表明从 src 或者 resources 目录下找。
    注意此时 @Value("") 的双引号中为 $ 符号而非 # 符号,{} 中为配置文件中的 key。
    ============================================================================
    public class ValuePropertiesMain {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = 
            new AnnotationConfigApplicationContext(ValuePropertiesConfig.class);
    
            ValuePropertiesConfig service = context.getBean(ValuePropertiesConfig.class);
            service.output();
    
            context.close();
        }
    }
    ============================================================================
    控制台打印如下:
    姓名:沉默王二 年纪:18
    ============================================================================
    如果出现乱码,请把文件格式设置为 UTF-8。
    

06、Bean 的初始化和销毁

  • 在实际开发中,经常需要在 Bean 初始化和销毁时加一些额外的操作,解释如何实现吗?”

    init-method、destroy-method

    新建 InitDestroyService 、InitDestroyConfig、InitDestroyMain,内容如下:

    public class InitDestroyService {
    
        public InitDestroyService() {System.out.println("构造方法");}
    
        public void init() {System.out.println("初始化");}
    
        public void destroy(){System.out.println("销毁");}
    }
    
    InitDestroyService() 为构造方法,init() 为初始化方法,destroy() 为销毁方法。
    ============================================================================
    @Configuration
    public class InitDestroyConfig {
        @Bean(initMethod = "init",destroyMethod = "destroy")
        public InitDestroyService initDestroyService() {
            return new InitDestroyService();
        }
    }
    
    @Bean 注解的 initMethod 用于指定 Bean 初始化的方法,destroyMethod 用于指定 Bean 销毁时的方法。
    ============================================================================
    public class InitDestroyMain {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = 
                new AnnotationConfigApplicationContext(InitDestroyConfig.class);
            InitDestroyService service = context.getBean(InitDestroyService.class);
            System.out.println("准备关闭容器");
            context.close();
        }
    }
    ============================================================================
    控制台打印如下:
    构造方法
    初始化
    准备关闭容器
    销毁
    

    也就是说,初始化方法在构造方法后执行,销毁方法在容器关闭后执行。

  • @PostConstruct、@PreDestroy

    新建 InitDestroyService、InitDestroyConfig、 InitDestroyMain 类,内容如下:

    public class InitDestroyService {
        public InitDestroyService() {System.out.println("构造方法");}
    
        @PostConstruct
        public void init() {System.out.println("初始化");}
    
        @PreDestroy
        public void destroy() {System.out.println("销毁");}
    }
    
    @PostConstruct 注解的作用和 @Bean 注解中 init-method 作用相同,用于指定 Bean 初始化后执行的方法。
    @PreDestroy 注解的作用和 @Bean 注解中 destroyMethod 作用相同,用于指定 Bean 被容器销毁后执行的方法。
    ============================================================================
    @Configuration
    public class InitDestroyConfig {
        @Bean
        public InitDestroyService initDestroyService() {
            return new InitDestroyService();
        }
    }
    @Bean 注解中不需要再指定  init-method 和 destroyMethod 参数了。
    ============================================================================
    public class InitDestroyMain {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = 
                new AnnotationConfigApplicationContext(InitDestroyConfig.class);
            InitDestroyService service = context.getBean(InitDestroyService.class);
            System.out.println("准备关闭容器");
            context.close();
        }
    }
    ============================================================================
    控制台打印如下:
    构造方法
    初始化
    准备关闭容器
    销毁  
    

07、为 Bean 配置不同的环境

  • Spring 开发中经常需要将 Bean 切换到不同的环境,比如开发环境、测试环境、正式生产环境,具体怎么实现的?”

    为开发环境和正式生产环境配置不同的数据源。

    新建 Datasource、Config 、Main类,内容如下:

    @Getter
    @Setter
    public class Datasource {
        
        private String dburl;
        public Datasource(String dburl) {this.dburl = dburl;}
    }
    dbname 用于指定不同环境下数据库的连接地址。
    ============================================================================
    @Configuration
    public class Config {
        @Bean
        @Profile("dev")
        public Datasource devDatasource() {
            return new Datasource("开发环境");
        }
    
        @Bean
        @Profile("prod")
        public Datasource prodDatasource() {
            return new Datasource("正式生产环境");
        }
    }
    ============================================================================
    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = 
                new AnnotationConfigApplicationContext();
            ConfigurableEnvironment environment = context.getEnvironment();
            environment.setActiveProfiles("prod");
            context.register(Config.class);
            context.refresh();
    
            Datasource datasource = context.getBean(Datasource.class);
            System.out.println(datasource.getDbUrl());
            context.close();
        }
    }
    ============================================================================
    控制台打印如下:
    正式生产环境
    
  • 以上的内容是转载于公众号:沉默王二。已经获取原作者授权,希望大家多多支持二哥,关注他的公众号和文章,写的很棒!!

posted @ 2020-04-04 16:38  RookieMZL  阅读(240)  评论(0编辑  收藏  举报