zoukankan      html  css  js  c++  java
  • [spring] SpEL

    简介

    我们书写的程序中,各个类之间有依赖的,需要手动实例化依赖类再赋给它。既然我们都通过IoC容器自动管理Bean了,每次使用都需要自己管理这样的依赖关系过于繁琐。

    于是就有了通过配置文件的方式,使其自动注入依赖的bean。

    spring提供3种装配的方式:xml装配java装配自动装配

    相比于xml装配,推荐的是使用java装配

    通常使用自动装配,减少配置文件。

    自动装配

    spring从2个角度实现自动装配:

    • 组件扫描:spring会自动发现应用上下文中所创建的bean。
    • 自动装配:spring自动将满足的bean装配。

    项目结构

    .
    ├── build.gradle
    └── src/
        ├── main/
        │   ├── java/
        │   │   └── com/
        │   │       └── yww/
        │   │           ├── Main.java
        │   │           ├── Message.java
        │   │           ├── ReaderConfig.java
        │   │           └── Reader.java
        │   └── resources/
        │       └── beans.xml
        └── test/
            ├── java/
            │   └── com/
            │       └── yww/
            │           └── MainTest.java
            └── resources/
    

    代码

    build.gradle项目构建配置。

    注释的部分是使用插件并直接gradle run命令运行项目。jar{}部分的配置,可以打包成一个用java命令运行的jar包。(注意需要指定好主类的全名)

    plugins {
        id 'java'
        // id 'application'
    }
    
    // mainClassName = 'com.yww.Main'
    group 'com.yww'
    version '1.0-SNAPSHOT'
    
    sourceCompatibility = 1.8
    
    repositories {
        mavenCentral()
    }
    
    ext{
        springVersion = '5.2.0.RELEASE'
    }
    
    dependencies {
        compile "org.springframework:spring-core:$springVersion"
        compile "org.springframework:spring-context:$springVersion"
        compile "org.springframework:spring-beans:$springVersion"
        compile "org.springframework:spring-expression:$springVersion"
        compile "org.springframework:spring-aop:$springVersion"
        compile "org.springframework:spring-aspects:$springVersion"
    
        testCompile "junit:junit:4.12"
        testCompile "org.springframework:spring-test:$springVersion"
    }
    
    jar {
        from {
            configurations.runtime.collect{zipTree(it)}
        }
        manifest {
            attributes 'Main-Class': 'com.yww.Main'
        }
    }
    

    Message.java通过使用@Component注解,在组件扫描时,注册bean。

    @Value注解简单直接赋值。不同于xml配置文件配置bean这么繁琐。

    package com.yww;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Component
    public class Message {
        @Value("---hello world---")
        private String msg;
    
        public String getMsg() {
            return msg;
        }
    
        public void setMsg(String msg) {
            this.msg = "this mssage is : " + msg;
        }
    
    }
    
    

    Reader.java依赖于Message.java,使用@Autowired注入Message这个bean到Reader中。

    此处可以将@Autowired写在成员变量上,也可写在构造函数上,二者选其一皆可。(构造函数自动连线可以做一些其它操作而已)

    package com.yww;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    @Component
    public class Reader {
    //    @Autowired
        private Message msg;
    
        @Autowired
        public Reader(Message msg){
            this.msg = msg;
        }
    
        public void print(){
            System.out.println(msg.getMsg());
        }
    
    }
    
    

    ReaderConfig.java配置文件,主要时用于在获取应用上下文时,设置此类可以完成开启其中的配置。其类名任意。这里主要是开启组件扫描,将默认同包名下带有@Component等注解的类注册为bean。

    习惯上,我们可以在此处做一些配置工作,比如设置初始值,或者如何装配。为了方便,初始值在前面已使用@Value设置。

    package com.yww;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ComponentScan
    public class ReaderConfig {
    
        // 可以在配置文件中设置初始值
        // 为了方便直接,就在Message的成员上使用@Value设置了值。
    //    @Bean
    //    public Message message(){
    //        Message message = new Message();
    //        message.setMsg("---hi---");
    //        return message;
    //    }
    }
    
    

    Main.java主函数,用于启动应用。这里展示如何获取到有依赖关系,并且按照自动装配好的bean。

    不同于web应用,app需要有个主函数启动应用,常规的web应用会打包成war放入tomcat加载,不过spring boot的web应用也可以有个主函数启动web应用。

    代码里注释的部分,是使用xml配置,演示如何获取bean。

    package com.yww;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class Main {
    
        public static void main(String[] args){
            // xml配置(beans.xml)
    //        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    //        Message msg = (Message) context.getBean("msg1");
    //        System.out.println(msg.getMsg());
    
            // java配置(ReaderConfig.java) + 获取组件扫描到的bean
            // 测试文件AppTest.java中,可以通过注解@ContextConfiguration加载配置文件上下文,方便使用@Autowired注解获取Bean。
            ApplicationContext context = new AnnotationConfigApplicationContext(com.yww.ReaderConfig.class);
            Reader reader = (Reader) context.getBean(Reader.class);
            reader.print();
        }
    }
    
    

    最后剩下测试文件MainTest.java,这里不多过问,只是可以使用@ContextConfiguration注解获取上下文的bean,在使用@Autowired自动注入bean,方便了测试。

    运行

    因为为了简单,项目是非web项目,很少有帖子讲解对此的打包和运行。(web项目就很容易通过添加war插件和gradle build打包)

    运行的方法尝试出了几种:

    • 方法1:使用idea直接运行main类即可,也可运行测试类MainTest.java

    • 方法2:使用gradle,需要在build.gradle文件中,添加插件application,并设置好主函数的名称mainClassName。最后在项目根目录下(build.gradle同级目录)执行命令:

    gradle run
    

    问题处理

    非web应用中,会发现一个问题,无法通过@Autowired获取到bean,这是由于非web应用无法知道bean,也没有提供相应的注解去处理,只能通过ApplicationContext应用上下文获取bean。而bean之间是可以通过@Autowired获取到的。

    java装配

    很多时候,通过组件扫描和自动装配实现自动化配置都是推荐的方式。但如果要将第三方库装配到自己的应用中,就无法使用@Component注解实现自动化装配。

    要想显式配置bean,可以在配置文件中使用@Bean获取。

    // src/main/java/com/yww/ReaderConfig.java
    // ...
    @Bean
    public Message message(){
        Message message = new Message();
        message.setMsg("---hi---");
        return message;
    }
    // ...
    

    使用自动装配,不能创建多个同类型的bean。

    Bean更名

    默认bean的ID与@Bean注解的方法名一样的。

    可提供name参数设置其它名称。

    @Bean(name="msgX")
    

    不使用组件扫描的配置

    如果不使用组件扫描,那么一个配置如何找到另一个配置,或者bean?

    此时,可以使用@Import导入那个类。

    package com.yww;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Import;
    
    @Configuration
    @Import({MessageConfig.class})
    public class ReaderConfig {
    
        // 可以在配置文件中设置初始值
        // 为了方便直接,就在Message的成员上使用@Value设置了值。
    //    @Bean
    //    @Bean(name="msgX")
    //    public Message message(){
    //        Message message = new Message();
    //        message.setMsg("---hi---");
    //        return message;
    //    }
    
        // 这个bean传入的名称可以任意.
        // 但如果在同一个类里声明了名称,像上面那样指定了名词,这里的参数名也必须相同。
        @Bean
        public Reader reader(Message msg){
            System.out.println(msg.getMsg());
            msg.setMsg("---hello---");
            return new Reader(msg);
        }
    
    }
    
    

    其中,导入的另一个bean的java配置文件如下。

    // src/main/java/com/yww/MessageConfig.java
    package com.yww;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class MessageConfig {
    
        @Bean
        public Message message(){
            return new Message();
        }
    }
    

    xml装配

    • gradle app的xml配置放在src/main/resources/目录下。
    • web的xml配置放在web/WEB-INF/目录下,需要在web.xml中配置<context-param>

    配置形如下面这个。

    <?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/
    
        <bean id="msg1" class="com.yww.Message">
            <property name="msg" value="hello world"/>
        </bean>
    
    </beans>
    

    如果不给id,将会通过全限定类名来命名,此处的bean将会被命名为com.yww.Message#0#0是计数同类型的其它bean。

    其它设置

    由于不推荐使用xml配置,故只是简单在此记下有哪些配置。

    很多配置有c-命名空间p-命名空间去替代繁杂的标签。

    • 构造其注入bean引用,注入字符串,数字,列表等。(<constructor-arg>)
    • 使用xml配置<bean>导入JavaConfig配置。
    • <beans>profile属性。
    • <jdbc><jee:jndi-lookup>
    • <beans>嵌套<beans>

    高级装配

    提供更多配置,实现更多装配的控制。

    Profile

    通常我们需要配置不同的环境,如:测试环境使用嵌入数据库h2,生产环境使用jndi获取数据库等。

    (从spring4开始)@Profile也是基于@Conditional实现。

    设置配置

    可以使用@Profile注解在方法上,去生成Bean。(此处使用JavaConfig配置,也可以使用xml配置)

    package com.yww;
    
    import org.springframework.context.annotation.*;
    
    @Configuration
    @ComponentScan
    @PropertySource("application.properties")
    public class ReaderConfig {
    
        @Bean
        @Profile("dev")
        public Message message1(){
            Message message = new Message();
            message.setMsg("---hi---1");
            return message;
        }
    
        @Bean
        @Profile("prod")
        public Message message2(){
            Message message = new Message();
            message.setMsg("---hi---2");
            return message;
        }
    }
    
    

    @PropertySource导入配置文件。

    激活配置

    通过两个属性确定哪个profile激活,spring.profiles.activespring.profiles.default

    如果设置了active,就用此值确定哪个profile激活。如果没设置active,就用default确定。如果都没设置,只会创建没有定义profile的bean。

    #src/main/resources/application.properties
    spring.profiles.default=dev
    spring.profiles.active=prod
    

    在测试类中,可以使用@ActiveProfiles("dev")便捷的激活配置。

    其它方式

    有多种方式设置属性:

    • DispatcherServlet的初始化参数。
    • Web应用的上下文参数。
    • JNDI条目。
    • 环境变量。
    • JVM的系统属性。
    • 集成测试类上,使用@ActiveProfiles注解设置。

    例,web.xml的配置。

        <!-- ... -->
        <context-param>
            <param-name>spring.profiles.default</param-name>
            <param-value>dev</param-value>
        </context-param>
        <!-- ... -->
    

    附:配置文件是如何关联的?

    程序如何找到这个.properties文件?

    在app项目中,我们通过@PropertySource注解到JavaConfig类上,设置.properties配置文件的路径。

    在gradle项目中,配置文件放在src/main/resources/路径下,还可以放在这个目录下的文件夹。如:src/main/resources/demo/app.properties的设置@PropertySource("demo/app.properties")

    在web项目中,spring web已经将配置文件设置好了,不需要@PropertySource配置。

    条件化的Bean

    如果希望实现一个bean只有在应用的类路径下包含特定的库时才创建,或者希望某个bean只有当在另一个特定bean声明之后才会创建,或者在设置了特定的环境变量后才会创建某个bean。

    通过@Conditional注解,它可以用于带有@Bean注解的方法上。如果条件计算为true就会创建bean,否则这个bean就会被忽略。

    条件化bean是spring boot自动装配的实现原理。

    示例

    当配置文件中含有"reader"值时,创建bean。

    目录结构

    .
    ├── build.gradle
    └── src/
        ├── main/
        │   ├── java/
        │   │   └── com/
        │   │       └── yww/
        │   │           ├── Main.java
        │   │           ├── Message.java
        │   │           ├── ReaderConfig.java
        │   │           ├── ReaderExistsCondition.java
        │   │           └── Reader.java
        │   └── resources/
        │       ├── application.properties
        └── test/
            ├── java/
            │   └── com/
            │       └── yww/
            │           └── MainTest.java
            └── resources/
    

    关键代码

    先取消了Reader.java类的@Component注解,方便此处使用条件化方式创建Reader的bean。

    设置的条件为ReaderExistsCondition.java,这个类继承接口Condition

    package com.yww;
    
    import org.springframework.context.annotation.*;
    
    @Configuration
    @ComponentScan
    @PropertySource("application.properties")
    public class ReaderConfig {
    
        @Bean
        public Message message1(){
            Message message = new Message();
            message.setMsg("---hi---");
            return message;
        }
    
        @Bean
        @Conditional(ReaderExistsCondition.class)
        public Reader reader(Message message){
            return new Reader(message);
        }
    }
    

    设置的条件为,从配置文件中寻找是否存在名为reader的配置属性,如果方法matches返回true,即会创建被设置条件的bean。

    package com.yww;
    
    import org.springframework.context.annotation.Condition;
    import org.springframework.context.annotation.ConditionContext;
    import org.springframework.core.env.Environment;
    import org.springframework.core.type.AnnotatedTypeMetadata;
    
    public class ReaderExistsCondition implements Condition {
    
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            Environment env = context.getEnvironment();
            return env.containsProperty("reader");
        }
    
    }
    

    附:接口Condition

    Condition接口源码如下,只包含一个matches方法,如果方法返回true即会满足条件创建bean,false即不会创建。

    @FunctionalInterface
    public interface Condition {
    
    	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
    
    }
    

    ConditionContext接口可以检查Bean,环境变量,资源,类。

    AnnotatedTypeMetadata接口能检查带有@Bean注解的方法上还有什么其它注解。

    处理自动装配的歧义性

    当同一个类型有多个bean被创建,组件扫描无法选取来装配。

    spring会抛出NoUniqueBeanDefinitionException

    解决方法

    有2种处理方法:

    • @Component@Bean上,使用@Primary注解,在遇到歧义时,选择首选的bean。
    • @Autowired@Inject上,使用@Qualifier限定要注入的bean的ID。当注解到@Component@Bean时,则是创建自己的限定符(类似于更名bean的ID)。

    @Qualifier限定名类似于设置标签给bean,spring会装载满足标签的bean。

    为了设置多个标签,可以自己定义注解了@Qualifier的接口,用自定义注解作为标签限定。(java 8允许在定义注解上添加@Repeatable实现重复注解,不过Spring的@Qualifier并没有为此添加重复注解功能。)

    附:byName和byType

    一般情况下,虽然创建的bean和注入的参数名不一样(此处的bean名message1和参数名message),但spring会以byType的方式找到类型相同的bean装配好。

    package com.yww;
    
    import org.springframework.context.annotation.*;
    
    @Configuration
    @ComponentScan
    public class ReaderConfig {
    
        @Bean
        public Message message1(){
            Message message = new Message();
            message.setMsg("---hi---1");
            return message;
        }
    
        @Bean
        public Reader reader(Message message){
            return new Reader(message);
        }
    }
    

    但有多个同类型的bean时,就无法依赖byType找到对应的bean,可以通过改变传入的参数名(此处改用参数名message2),采用byName指定bean。

    package com.yww;
    
    import org.springframework.context.annotation.*;
    
    @Configuration
    @ComponentScan
    public class ReaderConfig {
    
        @Bean
        public Message message1(){
            Message message = new Message();
            message.setMsg("---hi---1");
            return message;
        }
    
        @Bean
        public Message message2(){
            Message message = new Message();
            message.setMsg("---hi---2");
            return message;
        }
    
        @Bean
        public Reader reader(Message message2){
            return new Reader(message2);
        }
    }
    

    注入外部值

    可以通过3种方式获取环境值:

    • 注入并使用Environment
    • 属性占位符(property placeholder)。
    • Spring表达式语言SpEL
    #src/main/resources/application.properties
    info.message=this is a message.
    info.counter=10
    

    Environment:

    import org.springframework.context.annotation.*;
    import org.springframework.core.env.Environment;
    
    @Component
    @PropertySource("application.properties")
    public class Message {
    
        @Autowired
        Environment env;
    
        public void print(){
            String msgStr = env.getProperty("info.message", "this is a msg");
            int msgInt = env.getProperty("info.counter", Integer.class, 30);
        }
    
    }
    

    属性占位符:

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.stereotype.Component;
    
    @Component
    @PropertySource("application.properties")
    public class Message {
        @Value("${info.message}")
        private String msg;
    }
    

    Spring EL:

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.stereotype.Component;
    
    @Component
    @PropertySource("application.properties")
    public class Message {
        @Value("#{systemProperties['info.message']}")
        private String msg;
    }
    
  • 相关阅读:
    PC端微信扫码支付和支付宝跳转支付 斧头帮
    微信支付 斧头帮
    PC端实现浏览器点击分享到QQ好友,空间,微信,微博等 斧头帮
    图片,word,Excel等附件上传 斧头帮
    java定时任务详解 斧头帮
    手机浏览器实现分享给好友或是朋友圈 斧头帮
    Java缓存EhcacheEhcache的Cache在SSM框架中的配置 斧头帮
    通过精确地址获取经纬度 斧头帮
    SpringBoot中设置自定义拦截器 斧头帮
    Visual Studio 2008创建项目(ATL)
  • 原文地址:https://www.cnblogs.com/maplesnow/p/11628735.html
Copyright © 2011-2022 走看看