多环境配置

在日常开发中,我们项目会有多个环境。例如开发环境(develop)也就是我们研发过程中疯狂敲代码修BUG阶段,生产环境(production )项目开发得差不多了,可以放在服务器上跑了。不同的环境下,可能我们的配置文件也存在不同,但是我们不可能切换环境的时候又去重新写一次配置文件,所以我们可以将多个环境的配置文件提前写好,进行自由切换。

由于SpringBoot只会读取application.properties或是application.yml文件,那么怎么才能实现自由切换呢?SpringBoot给我们提供了一种方式,我们可以通过配置文件指定:

spring:
  profiles:
    active: dev

接着我们分别创建两个环境的配置文件,application-dev.ymlapplication-prod.yml分别表示开发环境和生产环境的配置文件,比如开发环境我们使用的服务器端口为8080,而生产环境下可能就需要设置为8081或是443端口,那么这个时候就需要不同环境下的配置文件进行区分:

server:
  port: 8080
server:
  port: 8081

这样我们就可以灵活切换生产环境和开发环境下的配置文件了。

SpringBoot自带的Logback日志系统也是支持多环境配置的,比如我们想在开发环境下输出日志到控制台,而生产环境下只需要输出到文件即可(比如当项目部署到服务器上以后,就不用打印日志信息了,因为没必要24小时盯着,有问题的话就到文件里去找即可),这时就需要进行环境配置:

<springProfile name="dev">
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>
</springProfile>

<springProfile name="prod">
    <root level="INFO">
        <appender-ref ref="FILE"/>
    </root>
</springProfile>

注意springProfile是区分大小写的!

那如果我们希望生产环境中不要打包开发环境下的配置文件呢,我们目前虽然可以切换开发环境,但是打包的时候依然是所有配置文件全部打包,这样总感觉还欠缺一点完美,因此,打包的问题就只能找Maven解决了,Maven也可以设置多环境:

<!--分别设置开发,生产环境-->
<profiles>
    <!-- 开发环境 -->
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <environment>dev</environment>
        </properties>
    </profile>
    <!-- 生产环境 -->
    <profile>
        <id>prod</id>
        <activation>
            <activeByDefault>false</activeByDefault>
        </activation>
        <properties>
            <environment>prod</environment>
        </properties>
    </profile>
</profiles>

接着,我们需要根据环境的不同,排除其他环境的配置文件:

<resources>
<!--排除配置文件-->
    <resource>
        <directory>src/main/resources</directory>
        <!--先排除所有的配置文件-->
        <excludes>
            <!--使用通配符,当然可以定义多个exclude标签进行排除-->
            <exclude>application*.yml</exclude>
        </excludes>
    </resource>

    <!--根据激活条件引入打包所需的配置和文件-->
    <resource>
        <directory>src/main/resources</directory>
        <!--引入所需环境的配置文件-->
        <filtering>true</filtering>
        <includes>
            <include>application.yml</include>
            <!--根据maven选择环境导入配置文件-->
            <include>application-${environment}.yml</include>
        </includes>
    </resource>
</resources>

接着,我们可以直接将Maven中的environment属性,传递给SpringBoot的配置文件,在构建时替换为对应的值:

spring:
  profiles:
    active: '@environment@'  #注意YAML配置文件需要加单引号,否则会报错

这样,根据我们Maven环境的切换,SpringBoot的配置文件也会进行对应的切换。

最后我们打开Maven栏目,就可以自由切换了,直接勾选即可,注意切换环境之后要重新加载一下Maven项目,不然不会生效!

打包运行

现在我们的SpringBoot项目编写完成了,那么如何打包运行呢?非常简单,只需要点击Maven生命周期中的package即可,它会自动将其打包为可直接运行的Jar包,第一次打包可能会花费一些时间下载部分依赖的源码一起打包进Jar文件。

我们发现在打包的过程中还会完整的将项目跑一遍进行测试,如果我们不想测试直接打包,可以手动使用以下命令:

mvn package  -DskipTests

打包后,我们会直接得到一个名为springboot-study-0.0.1-SNAPSHOT.jar的文件,这时在CMD窗口中输入命令:

java -jar springboot-study-0.0.1-SNAPSHOT.jar

输入后,可以看到我们的Java项目成功运行起来了,如果手动关闭窗口会导致整个项目终止运行。


再谈Spring框架

注意: 开始本部分前,建议有SSM阶段的Spring源码基础。

我们在SpringBoot阶段,需要继续扩充Spring框架的相关知识,来巩固和强化对于Spring框架的认识。

任务调度

为了执行某些任务,我们可能需要一些非常规的操作,比如我们希望使用多线程来处理我们的结果或是执行一些定时任务,到达指定时间再去执行。

这时我们首先想到的就是创建一个新的线程来处理,或是使用TimerTask来完成定时任务,但是我们有了Spring框架之后,就不用这样了,因为Spring框架为我们提供了更加便捷的方式进行任务调度。

异步任务

需要使用Spring异步任务支持,我们需要在配置类上添加@EnableAsync或是在SpringBoot的启动类上添加也可以。

@EnableAsync
@SpringBootApplication
public class SpringBootWebTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootWebTestApplication.class, args);
    }
}

接着我们只需要在需要异步执行的方法上,添加@Async注解即可将此方法标记为异步,当此方法被调用时,会异步执行,也就是新开一个线程执行,不是在当前线程执行。

@Service
public class TestService {

    @Async
    public void test(){
        try {
            Thread.sleep(3000);
            System.out.println("我是异步任务!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
@RequestMapping("/login")
public String login(HttpServletRequest request){
    service.test();
    System.out.println("我是同步任务!");
    return "login";
}

实际上这也是得益于AOP机制,通过线程池实现,但是也要注意,正是因为它是AOP机制的产物,所以它只能是在Bean中才会生效!

使用 @Async 注释的方法可以返回 ‘void’ 或 “Future” 类型,Future是一种用于接收任务执行结果的一种类型,我们会在Java并发编程中进行讲解,这里暂时不做介绍。

定时任务

看完了异步任务,我们接着来看定时任务,定时任务其实就是指定在哪个时候再去执行,在JavaSE阶段我们使用过TimerTask来执行定时任务。

Spring中的定时任务是全局性质的,当我们的Spring程序启动后,那么定时任务也就跟着启动了,我们可以在配置类上添加@EnableScheduling或是在SpringBoot的启动类上添加也可:

@EnableAsync
@EnableScheduling
@SpringBootApplication
public class SpringBootWebTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootWebTestApplication.class, args);
    }
}

接着我们可以创建一个定时任务配置类,在配置类里面编写定时任务:

@Configuration
public class ScheduleConfiguration {

    @Scheduled(fixedRate = 2000)
    public void task(){
        System.out.println("我是定时任务!"+new Date());
    }
}

我们注意到 @Scheduled中有很多参数,我们需要指定’cron’, ‘fixedDelay(String)’, or 'fixedRate(String)'的其中一个,否则无法创建定时任务,他们的区别如下:

  • fixedDelay:在上一次定时任务执行完之后,间隔多久继续执行。
  • fixedRate:无论上一次定时任务有没有执行完成,两次任务之间的时间间隔。
  • cron:使用cron表达式来指定任务计划。

这里重点讲解一下cron表达式:

监听器

监听器对我们来说也是一个比较陌生的概念,那么何谓监听呢?

监听实际上就是等待某个事件的触发,当事件触发时,对应事件的监听器就会被通知。

@Component
public class TestListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println(event.getApplicationContext());
    }
}

通过监听事件,我们就可以在对应的时机进行一些额外的处理,我们可以通过断点调试来查看一个事件是如何发生,以及如何通知监听器的。

通过阅读源码,我们得知,一个事件实际上就是通过publishEvent方法来进行发布的,我们也可以自定义我们自己项目中的事件,并注册对应的监听器进行处理。

public class TestEvent extends ApplicationEvent {   //需要继承ApplicationEvent
    public TestEvent(Object source) {
        super(source);
    }
}
@Component
public class TestListener implements ApplicationListener<TestEvent> {

    @Override
    public void onApplicationEvent(TestEvent event) {
        System.out.println("自定义事件发生了:"+event.getSource());
    }
}
@Resource
ApplicationContext context;

@RequestMapping("/login")
public String login(HttpServletRequest request){
    context.publishEvent(new TestEvent("有人访问了登录界面!"));
    return "login";
}

这样,我们就实现了自定义事件发布和监听。

Aware系列接口

我们在之前讲解Spring源码时,经常会发现某些类的定义上,除了我们当时讲解的继承关系以外,还实现了一些接口,他们的名称基本都是xxxxAware,比如我们在讲解SpringSecurity的源码中,AbstractAuthenticationProcessingFilter类就是这样:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
    protected ApplicationEventPublisher eventPublisher;
    protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    private AuthenticationManager authenticationManager;
    ...

我们发现它除了继承自GenericFilterBean之外,还实现了ApplicationEventPublisherAware和MessageSourceAware接口,那么这些Aware接口到底是干嘛的呢?

Aware的中文意思为感知。简单来说,他就是一个标识,实现此接口的类会获得某些感知能力,Spring容器会在Bean被加载时,根据类实现的感知接口,会调用类中实现的对应感知方法。

比如AbstractAuthenticationProcessingFilter就实现了ApplicationEventPublisherAware接口,此接口的感知功能为事件发布器,在Bean加载时,会调用实现类中的setApplicationEventPublisher方法,而AbstractAuthenticationProcessingFilter类则利用此方法,在Bean加载阶段获得了容器的事件发布器,以便之后发布事件使用。

public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
    this.eventPublisher = eventPublisher;   //直接存到成员变量
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authResult);
    SecurityContextHolder.setContext(context);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
    }

    this.rememberMeServices.loginSuccess(request, response, authResult);
  	//在这里使用
    if (this.eventPublisher != null) {
        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }

    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

同样的,除了ApplicationEventPublisherAware接口外,我们再来演示一个接口,比如:

@Service
public class TestService implements BeanNameAware {
    @Override
    public void setBeanName(String s) {
        System.out.println(s);
    }
}

BeanNameAware就是感知Bean名称的一个接口,当Bean被加载时,会调用setBeanName方法并将Bean名称作为参数传递。

有关所有的Aware这里就不一一列举了。