ramostear.comramostear.com 谭朝红的技术分享博客

格言 编程是一门技术,也是一门艺术 !

(译)使用Spring Boot和Axon实现CQRS&Event Sourcing

(译)使用Spring Boot和Axon实现CQRS&Event Sourcing

上一篇中,我们讲述了CQRS和Event Sourcing的相关概念以及他们能解决什么问题。尽管可以在不适用任何其他框架或库的情况下实现CQRS/ES,但我们还是建议使用已有的一些工具。这些工具可以简化开发过程,同时运行开发人员专注于业务逻辑的处理,避免重复的造轮子。在本节中,我们将选择Axon框架来实现CQRS/ES。

什么是Axon?

Axon是一个轻量级的Java开源框架,可以帮助构建你构建基于CQRS模式的可伸缩、可扩展和库维护的Java应用程序。它还可以帮助你准备Event Sourcing所需要的环境。Axon提供了所有重要构建模块的实现,如聚合、存储库,命令和时间总线。Axon可以让开发人员的工作更为轻松。

为何选择Axon?

Axon让我们避免了负载的配置和对数据流的操作,我们可以专注于应用程序业务规则的定制,而不是创建样板代码。使用Axon可以获得如下的一些优势:

  • 优化事件处理:我们应该注意到,事件的发布具有先后顺序。Axon框架保证了事件被执行的先后顺序。
  • 内置测试环境:Axon提供了一个测试工具集,允许在特定的时间上对系统进行测试,这使得单元测试更为容易。
  • Spring Boot AutoConfiguration:在Spring Boot应用程序中配置Axon是一件非常简单的事情。只需要提供必要的依赖项,Axon将会自动配置一些基本组件。
  • 支持注解:Axon提供了注解支持,这使得我们的代码更清晰可读,我们可以使用这些注解轻松构建聚合和事件处理程序,而无需关心Axon特定的处理逻辑

Spring Boot应用程序中快速配置Axon

在默认情况下,Axon以经提供了对Spring Boot的集成支持。我们只需要通过一些简单的配置步骤就可以将Axon与Spring Boot整合在一起。

步骤1

第一步是使用合适的项目构建工具在项目中配置Axon依赖项。以下是使用Gradle来配置Axon的方法:

dependencies{
    compile("org.axonframework:axon-spring-boot-starter:3.2")
    compile("org.axonframework:axon-mongo:3.2")
    testCompile("org.axonframework:axon-test:32.")
}

第一个依赖项为我们提供了与Spring Boot集成的最基本的Axon所有必要组件,如命令中线,事件总线和聚合。第二个依赖项是为我们的聚合或事件配置库提供所需的基本环境。最后一个依赖项用于构建先关的测试环境。

步骤2

根据需要配置Axon所需要的一些Spring Bean。比如EventHandlerConfiguration(负责控制事件处理程序行为的组件),如果其中有一个事件执行失败,则终止处理后续的所有事件。当然这是非必须的,但是还是值得在应用程序中进行此配置,以防止系统中数据的不一致。配置代码如下:

@Configuration
public class AxonConfig {

private final EventHandlingConfiguration eventHandlingConfiguration;

@Autowired
public AxonConfig(EventHandlingConfiguration eventHandlingConfiguration) {
   this.eventHandlingConfiguration = eventHandlingConfiguration;
}

@PostConstruct
public void registerErrorHandling() {
   eventHandlingConfiguration.configureListenerInvocationErrorHandler(configuration -> (exception, event, listener) -> {
       String msg = String.format(
               "[EventHandling] Event handler failed when processing event with id %s. Aborting all further event handlers.",
               event.getIdentifier());
       log.error(msg, exception);
       throw exception;
   });
}}

这里的主要思想是创建一个额外的配置文件(使用@Configuration注释的类)。该类的构造函数注入由Spring自身所管理的EventHandlingConfiguration依赖项。由于绑定依赖,我们可以在此对象上调用configureListenerInvocationErrorHandler()并通过记录异常将异常传播到上层去处理错误。

步骤3

我们使用MongoDB来存储Event Store中所发生的所有事件。要实现此功能,可以通过如下的方法来实现:

@Bean
public EventStorageEngine eventStore(MongoTemplate mongoTemplate) {
   return new MongoEventStorageEngine(
           new JacksonSerializer(), null, mongoTemplate, new DocumentPerEventStorageStrategy());
}

这样,在事件总线上发布的所有事件都将自动保存到MongoDB数据库中。通过这种简单的配置,我们就可以在应用程序中使用MongoDB的数据源。

在Axon配置方面就是这样的简单。当然,我们还有其他很多的配置方式。但我们可以通过上述简单的配置,就可以使用Axon的功能了。

使用Axon实现CQRS

根据上图,创建命令,将命令传递给命令总线然后创建事件并将事件放在事件总线上还不是CQRS。我们必须记住改变写入存储库的状态并从读取数据库中读取当前状态,这才是CQRS模式的关键点。

配置此流程也并不复杂。在将命令传递给命令网关时,Spring将名利类型作为参数以搜索带有@CommandHandler 注释的方法。

@Value
class SubmitApplicationCommand {
   private String appId;
   private String category;
}

@AllArgsConstructor
public class ApplicationService {
   private final CommandGateway commandGateway;

   public CompletableFuture<Void> createForm(String appId) {
       return CompletableFuture.supplyAsync(() -> new SubmitExpertsFormCommand(appId, "Android"))
               .thenCompose(commandGateway::send);
   }
}

除其他事项外,命令处理程序负责将创建的事件发送到事件总线。它将事件对象放置到AggregateLifecycle静态导入的apply()方法中。然后调度该事件以查找预期的处理程序,并且由于我们配置了事件存储库,所有事件都自动保存在数据库中。

@Value
class ApplicationSubmittedEvent {
   private String appId;
   private String category;
}

@Aggregate
@NoArgsConstructor
public class ApplicationAggregate {
   @AggregateIdentifier
   private String id;

   @CommandHandler
   public ApplicationAggregate(SubmitApplicationCommand command) {
      //some validation
       this.id = command.getAppId;
       apply(new ApplicationSubmittedEvent(command.getAppId(), command.getCategory()));
   }
}

要更改写库的状态,我们需要提供一个使用@EventHandler注释的方法。该应用程序可以包含多个事件处理程序。他们每个人都应该执行一个特定的任务,如发送电子邮件,记录或保存在数据库中。

@RequiredArgsConstructor
@Order(1)
public class ProjectingEventHandler {
   private final IApplicationSubmittedProjection projection;

   @EventHandler
   public CompletableFuture<Void> onApplicationSubmitted(ExpertsFormSubmittedEvent event) {
       return projection.submitApplication(event.getApplicationId(), event.getCategory());
   }

如果我们想确定所有事件处理程序的处理顺序,我们可以用@Order注释一个类并设置一个序列号。submitApplication()方法负责进行所有必要的更改并将新的数据存储到写库中。

这些都是使我们的应用程序采用CQRS模式原则的关键点。当然,这些原则只能应用于我们应用程序的某些部分,具体取决于业务需求。Event Sourcing不适合我们正在构建的每个应用程序或模块。在实现此模式时也要谨慎,因为更复杂的应用程序可能难以维护。

总结

使用Axon框架,CQRS和Event Sourcing的实现变得简单化。有关高级配置的更多详细信息,请访问Axon的网站https://docs.axonframework.org/

原文作者:ŁukaszKucik

原文地址:https://www.nexocode.com/blog/posts/smooth-implementation-cqrs-es-with-sping-boot-and-axon/

译 者:谭朝红

译文地址:https://www.ramostear.com/articles/impl_cqrs_es_by_spring_boot_and_axon.html

(译)CQRS & Event Sourcing — 解决检索应用程序状态问题的一剂良方

(译)CQRS & Event Sourcing — 解决检索应用程序状态问题的一剂良方

CQRS & Event Sourcing — 解决检索应用程序状态问题的一剂良方

现在,每个开发人员都很熟悉MVC标准体系结构设计模式。大多数的应用程序都是基于这种体系结构进行创建的。它允许我们创建可扩展的大型企业应用程序,但近期我们还听到了另外的一些有关于CQRS/ES的相关信息。这些方法应该被放在MVC中一起使用吗?他们可以解决什么问题?现在,让我们一起来看看CQRS/ES是什么,以及他们都有哪些优点和缺点。

CQRS — 模式介绍

CQRS(Command Query Responsibility Segregation)是一种简单的设计模式。它衍生与CQS,即命令和查询分离,CQS是由Bertrand Meyer所设计。按照这一设计概念,系统中的方法应该分为两种:改变状态的命令和返回值的查询。Greg young将引入了这个设计概念,并将其应用于对象或者组件当中,这就是今天所要将的CQRS。它背后的主要思想是应用程序更改对象或组件状态(Command)应该与获取对象或者组件信息(Query)分开。

下面,将通一张图来说明应用程序中有关CQRS部分的组成结构:

CQRS模式介绍

Commands(命令)—表示用户的操作意图。它们包含了与用户将要对系统执行操作的所有必要信息。

  • Command Bus(命令总线):是一种接收命令并将命令传递给命令处理程序的队列。
  • Command Handler(命令处理程序):包含实际的业务逻辑,用于验证和处理命令中接收到的数据。Command handler负责生成和传播域事件(Event)到事件总线(Event Bus)。
  • Event Bus(事件总线):将事件发布给订阅特定事件类型的事件处理程序。如果存在连续的事件依赖,事件总线可以使用异步或者同步的方式将事件发布出去。
  • Event Handler(事件处理程序):负责处理特定类型的事件。它们的职责是将应用程序的最新状态保存到读库中,并执行终端的相关操作,如发送电子邮件,存储文件等。

Query(查询):表示用户实际可用的应用程序状态。获取UI的数据应该通过这些对象完成。

下面我们将介绍有关CQRS的诸多优点,它们是:

  • 我们可以给处理业务逻辑部分和处理查询部分的开发人员分别分配任务,但需要小心的是,这种模式可能会破坏信息的完整性。
  • 通过在多个不同的服务器上扩展Commands和Query,我们可以进一步提升应用程序的读/写性能。
  • 使用两个不同的数据库(读库/写库)进行同步,可以实现自动备份,无需额外的干预工作。
  • 读取数据时不会涉及到写库的操作,因此在使用事件源是读数据操作会更快。
  • 我们可以直接为视图层构建数据,而无需考虑域逻辑,这可以简化视图层的工作并提高性能。

尽管使用CQRS模式具有上述诸多的优点,但是在使用前还需要慎重考虑。对于只具有简单域的简单项目,其UI模型与域模型紧密联系的,使用CQRS反而会增加项目的复杂度和冗余度,这无疑是过度的设计项目。此外,对于数据量较少或者性能要求较低的项目实施CQRS模式不会带来显著的性能提升。

Event Sourcing — 案例研究

有这样一个案例,我们想要检索任何一个域对象的历史状态数据,而且在任何时间都可以生成统计数据。我们想要检查上个月、上个季度或者过去任何时间的状态汇总。想要解决这个问题并不容易。我们可以在特定的时间范围内将额外的数据保存在数据库中,但这种方法也存在一些缺点。我们不知道范围应该是什么样子,以及未来统计数据需要哪些数据项。为了避免这些问题,我们可以每天为所有聚合创建快照,但它们同样会产生大量的冗余数据。

Event Sourcing(ES)似乎是目前解决这些问题的最佳方案。Event Sourcing允许我们将Aggregate(聚合)状态的每一个更改事件保存在Event Store的事件存储库中。通过Command Handler将事件写入到事件存储库中,并处理相关的逻辑。要创建Aggregate(聚合)对象的当前状态,我们需要运行创建预期域对象的所有事件并对其执行所有的更改。下面我们将通过一张图来说明这一架构设计方式:

event-sourcing

下面我们将列举一些使用ES的优点:

  • 时间穿梭机:可以及时重建特定聚合的状态。每个事件都包含一个时间戳。根据这些时间戳可以在特定的时间内运行事件或者停止事件。
  • 自动审计:我们不需要额外的工作就可以检查出在特定的时间范围内谁做了什么以及改变了什么。这和可以显示更改历史记录的系统日志不同,事件可以告知我们每次更改背后所对应的操作意图。
  • 易于引入纠正措施:当数据库中的数据发生错误时,我们可以将应用程序的状态回退到特定的时间点上,并重建当时的应用程序状态。
  • 易于调试:如果应用程序出现问题,我们可以将特定事件内的所有事件取出,并逐条的重建应用状态,以检查应用程序可能出现问题的地方。这样我们可以更快的找到问题,缩短调试所需的时间。

Aggregates

Aggregate(聚合)一词在本文中多次被提及,那它到底是什么意思?Aggregate(聚合)来自于领域驱动设计(DDD)的一个概念,它指的是始终保持一致状态的实体或者相关实体组。我们可以简单的理解为接收和处理Command(包含Command Handler)的一个边界,然后根据当前状态生成事件。在通常情况下,Aggregate root(聚合根)由一个域对象构成,但它可以由多个对象组成。我们还需要注意整个应用程序可以包含多个Aggregate(聚合),并且所有事件都存储在同一个存储库中。

总结

CQRS/ES可以作为特定问题的解决方案。它可以在标准N层架构设计的应用程序的某些层中进行引入,它可以解决非标准问题,常规架构中我们所拿到的是最终状态,在很多情况下,固然当前状态很重要,但我们还需要知道当前状态是如何产生的。CQRS和ES两种概念应该一起使用吗?事实表明,并没有。我们想要统计任何时间范文内的域对象状态,而写库只能存储当前状态。引入CQRS并没能帮助我们解决这一问题。在下一章节中,我们将引入Axon框架,Axon框架时间了CQRS/ES,用于解决某些域对象的一些特定问题,尤其是收集历史统计数据。我们将阐述如何使用Axon框架实现CQRS/ES并实现与Spring Boot应用程的整合。

作者:LukaszKucik ,译:谭朝红,原文:CQRS and Event Sourcing as an antidote for problems with retrieving application states

WebMvcConfigurerAdapter被弃用后的两个选择

WebMvcConfigurerAdapter被弃用后的两个选择

WebMvcConfigurerAdapter类型被弃用后的选择

1. 介绍

在本文中,将介绍将spring 4.xx(或者更低)版本升级到Spring 5.xx以及将Spring Boot 1.xx版本升级到Spring Boot 2.xx版本后会报的一个严重警告:“Warning:The type WebMvcConfigurerAdapter is deprecated.” ,以及快速的分析产生这个严重警告的原因和处理办法。

2. 出现警告的原因

如果我们使用Spring 5.xx(或者Spring Boot 2.xx)版本来构建或者升级应用程序,在配置WebMvc时,则会出现此警告,这是因为在早期的Spring版本中,如果要配置Web应用程序,可以通过扩展WebMvcConfigurerAdapter类快熟实现配置,大致代码如下:

package com.ramostear.page;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 * Spring 4(或者Spring Boot 1.x)版本配置Web应用程序示例
 * @author ramostear
 * @create-time 2019/4/18 0018-1:38
 */
@Configuration
public class OldMvcConfig extends WebMvcConfigurerAdapter{

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        super.configurePathMatch(configurer);
        configurer.setUseSuffixPatternMatch(false);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/META-INF/resources/")
                .addResourceLocations("classpath:/public/")
                .addResourceLocations("classpath:/resources/");
        super.addResourceHandlers(registry);
    }
}

WebMvcConfigurerAdapter 是一个实现了WebMvcConfigurer 接口的抽象类,并提供了全部方法的空实现,我们可以在其子类中覆盖这些方法,以实现我们自己的配置,如视图解析器,拦截器和跨域支持等…,由于Java的版本更新,在Java 8中,可以使用default关键词为接口添加默认的方法,Spring在升级的过程中也同步支持了Java 8中这一新特性。下面是在Java 8 中给接口定义一个默认方法的简单实现:

public interface MyInterface{

    default void sayHello(){
        //...
    }

    void sayName(){}

    String writeName(){}

    //...
}

3. 解决方案

如前面所述,从Spring 5开始,WebMvcConfigure接口包含了WebMvcConfigurerAdapter类中所有方法的默认实现,因此WebMvcConfigurerAdapter这个适配器就被打入冷宫了,下面是WebMvcConfigurerAdapter类部分源码示例:

/**
 * An implementation of {@link WebMvcConfigurer} with empty methods allowing
 * subclasses to override only the methods they're interested in.
 *
 * @author Rossen Stoyanchev
 * @since 3.1
 * @deprecated as of 5.0 {@link WebMvcConfigurer} has default methods (made
 * possible by a Java 8 baseline) and can be implemented directly without the
 * need for this adapter
 */
@Deprecated
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {

    /**
     * {@inheritDoc}
     * <p>This implementation is empty.
     */
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
    }

    /**
     * {@inheritDoc}
     * <p>This implementation is empty.
     */
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    }

    ...
}

趣味提示:我们可以通过实现WebMvcConfigure接口中的方法来配置Web应用程序,而不需要让WebMvcConfigurerAdapter这个中间商 赚差价。

如此这般,我们找到了一个消除警告的方法:直接实现WebMvcConfigurer接口。在我们准备与WebMvcConfigurer打交道之前,先看看此接口的基本情况:

public interface WebMvcConfigurer {

    default void configurePathMatch(PathMatchConfigurer configurer) {
    }

    /**
     * Configure content negotiation options.
     */
    default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    }

    /**
     * Configure asynchronous request handling options.
     */
    default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    }

   ...
}

现在,我们就可以动手配置Web应用程序了,大致的代码如下:

/**
 * Spring 5 (或者Spring Boot 2.x)版本配置Web应用程序示例
 * @author ramostear
 * @create-time 2019/4/18 0018-1:40
 */
@Configuration
public class MvcConfigure implements WebMvcConfigurer{

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseSuffixPatternMatch(false);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/public/")
                .addResourceLocations("classpath:/resources/");
    }
}

就这样简单地将警告消除了,将原来的继承WebMvcConfigurerAdapter类改为实现WebMvcConfigurer接口,其余的地方都没有变化。但有一点需要注意,如果你是升级旧有的应用程序,需要将方法中对super()的调用代码清除。

至此,我们的程序又可以愉快的玩耍了。那么,除了消除中间商 赚差价的方式来规避警告外,还有没有其他的途径呢?答案当然是肯定的。我们除了消除中间商从WebMvcConfigurer中获得配置Web应用程序的途径外,还可以直接从WebMvcConfigurationSupport这个配置“供应商“的手中获取配置途径。WebMvcConfigurationSupport是一个提供了以Java编程方式来配置Web应用程序的配置主类,所以我们可以从这个配置供应商的手中获取Web应用程序的配置方式。方法很简单,只需要扩展此类并重写对应的方法即可。和上面的方式一样,我们先看看此类的内部大致结构:

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
    ...
    /**
     * Provide access to the shared handler interceptors used to configure
     * {@link HandlerMapping} instances with.
     * <p>This method cannot be overridden; use {@link #addInterceptors} instead.
     */
    protected final Object[] getInterceptors() {
        ...
    }

    /**
     * Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped
     * resource handlers. To configure resource handling, override
     * {@link #addResourceHandlers}.
     */
    @Bean
    @Nullable
    public HandlerMapping resourceHandlerMapping() {
        ...
        handlerMapping.setPathMatcher(mvcPathMatcher());
        handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
        handlerMapping.setInterceptors(getInterceptors());
        handlerMapping.setCorsConfigurations(getCorsConfigurations());
        return handlerMapping;
    }
}

是不是看到很熟悉的东西,有拦截器,静态资源映射等等…,现在我们只需要扩展此类并重写其中的方法,就可以配置我们的Web应用程序(还需要使用@Configuration对扩展类进行注释),示例代码如下:

/**
 * 消除警告的第二种配置选择
 * @author ramostear
 * @create-time 2019/4/7 0007-4:10
 */
@Configuration
public class MvcConfig extends WebMvcConfigurationSupport {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        super.configurePathMatch(configurer);
        configurer.setUseSuffixPatternMatch(false);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/META-INF/resources/")
                .addResourceLocations("classpath:/public/")
                .addResourceLocations("classpath:/resources/");
        super.addResourceHandlers(registry);
    }
}

4. 结束语

在本文中,通过快速的梳理,给出了两种不同的方案来消除由于升级Spring(或者Spring Boot)版本所带来的WebMvcConfigurerAdapter类被弃用的严重警告。本次技术分享到这里就结束了,感谢你耐心的阅读。如果你在遇到同样问题时还有更好的解决方案,可以在下方的评论区给我留言。

基于Base64编/解码算法的Spring Boot文件上传技术解析

基于Base64编/解码算法的Spring Boot文件上传技术解析

导读

文件上传时Web应用最为常见的功能之一,传统的文件上传需要定制一个特殊的form表单来上传文件,以上传图片为例,常规的做法是先上传图片,然后回传图片地址,最后在使用图片。这无疑会带来一个严重的问题:如果在接下来使用图片的过程中web请求中断了或者其他原因导致请求关闭,那么在服务器上就会遗留下未被使用的脏数据,还需要通过其他的方式进行清理。我将这种设计模式称之为“粗犷型经济”模式,不管市场(业务)是否消费,先生产(上传)了再说,最后会导致资源的极度浪费。而本次分享要谈的是另外一种设计模式,我称之为“节约型经济”模式,将生产活动(上传)以“责任承包”制度承包(下方)给具体的业务,采用Base64解码算法的方式,通过二进制文本同步传输到业务方法,最后将文件解码存储,以达到节约资源的效果。

基本术语

1. Base64编码

Base64编码是从二进制到字符的过程,可用于在HTTP环境下传递较长的标识信息。例如,在Java Persistence系统Hibernate中,就采用Base64来将一个较长的唯一标识符(一般为128-bit的UUID)编码成一个字符串,用作HTTP表单和HTTP GET URL中的参数。在其他的应用场景中,也常常需要把二进制数据编码为合适放在URL(包括隐藏表单域)中的形式。此时,采用Base64编码具有不可读性,需要解码后才能阅读。

2. 文件上传

文件上传就是将信息从个人计算机(本地计算机)传送到中央服务器(远程计算机)系统上,让网络上的其他用户可以进行访问。文件上传又分为Web上传和FTP上传,前者直接通过点击网页上的连接即可操作,后者需要专门的FTP工具进行操作。

案例解析

以添加文章的需求为一个案例,一篇文章需要有ID,标题,封面,简介,正文等信息。针对文章封面的设置,通常的做法是在添加文章的页面中通过异步的方式先将图片上传至服务器,然后回传图片存储地址(URL或者URI)绑定到一个隐藏域中和一个用于预览的IMG节点上。此时,文章主体信息是没有提交到服务器的,但与文章相关的图片已经先于文章到达了服务器,这就好比你想要去洗手间放翔,结果翔还没有出来,先从嘴里呕吐了一些东东。虽然看起来都是一个“异化”过程,但总觉得让人“恶心”。原本放完翔(提交请求)冲一下马桶(提交事务)就完事了,你现在还需要额外的擦拭一下地上的呕吐物(清理垃圾文件)。

基于上述的一个应用背景,提出了采用Base64编/解码的方式同步上传文件,让文章的图片随文章主体信息一起到达服务端,如果在请求的过程中服务意外终止,那么在服务器上也不会产生任何脏数据。需求和出发点就聊这么多,接下来进入本次分享的正题,看看如何实现同步上传文件的功能。

功能实现

1. 解码器

我们需要定义一个解码器对前端传入的二进制的图片数据进行解码,对于前端如何将图片文件采用Base64算法编码,在接下来的内容当中单独介绍。此时解码器的做用主要是获取Base64编码的二进制文本中header信息(编码方式)和文件类型信息。然后对数据域进行解码。完成解码工作后,再讲字节码转换成我们熟悉的MultipartFile类型对象。解码器的实现代码如下:

package com.ramostear.jfast.common.utils;

import org.springframework.web.multipart.MultipartFile;

import java.io.*;

/**
 * @author ramostear|谭朝红
 * @create-time 2019/3/19 0019-23:54
 * @modify by :
 * @since:
 */
public class Base64Decoder implements MultipartFile{

    private final byte[] IMAGE;

    private final String HEADER;

    private Base64Decoder(byte[]image,String header){
        this.IMAGE = image;
        this.HEADER = header;
    }

    public static MultipartFile multipartFile(byte[]image,String header){
        return new Base64Decoder(image,header);
    }

    @Override
    public String getName() {
        return System.currentTimeMillis()+Math.random()+"."+HEADER.split("/")[1];
    }

    @Override
    public String getOriginalFilename() {
        return System.currentTimeMillis()+(int)Math.random()*10000+"."+HEADER.split("/")[1];
    }

    @Override
    public String getContentType() {
        return HEADER.split(":")[1];
    }

    @Override
    public boolean isEmpty() {
        return IMAGE == null || IMAGE.length == 0;
    }

    @Override
    public long getSize() {
        return IMAGE.length;
    }

    @Override
    public byte[] getBytes() throws IOException {
        return IMAGE;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return new ByteArrayInputStream(IMAGE);
    }

    @Override
    public void transferTo(File file) throws IOException, IllegalStateException {
        new FileOutputStream(file).write(IMAGE);
    }
}

2. 转换器

现在,需要定义一个转换器,将前端传入的图片字符信息转换成Base64编码的字节数组,然后调用解码器获得最终的MultipartFile类型对象。转换器的实现比较简单,器代码如下:

package com.ramostear.jfast.common.utils;

import org.springframework.web.multipart.MultipartFile;

import java.util.Base64;

/**
 * @author ramostear|谭朝红
 * @create-time 2019/3/20 0020-0:00
 * @modify by :
 * @since:
 */
public class Base64Converter {

    public static MultipartFile converter(String source){
        String [] charArray = source.split(",");
        Base64.Decoder decoder = Base64.getDecoder();
        byte[] bytes = new byte[0];
        bytes = decoder.decode(charArray[1]);
        for (int i=0;i<bytes.length;i++){
            if(bytes[i]<0){
                bytes[i]+=256;
            }
        }
        return Base64Decoder.multipartFile(bytes,charArray[0]);
    }
}

重点介绍一下转换器的方法:

首先我们先看看基于Base64算法编码后的图片二进制字符的格式:

data:image/png;base64,iVBOR....Px1yGQ9EOFXNAAAAAE1FTkSuQmcc

因此,先通过“,”分割字符串,拿到数据的头部信息data:image/png;base64 ,再将数据的主体部分通过Base64进行转码,获得一个byte数组,最后调用解码器的解码方法获取MultipartFile对象。

3. 前端的Base64编码

后端的核心逻辑已经完成,接下来将介绍前端如何将一张图片采用Base64算法进行编码。

    1. 首先,需要有一个添加文章的form表单,同时将图片域设置为隐藏状态,提供一个图片预览的dom节点和一个浏览本地图片的input输入框,表单的核心代码如下:
    ...
     <form action="/articles" method="POST">
         ...
         <div class="file-preview">
              <div class="file-upload-zone">
                    <div class="file-upload-zone-title">Upload & preview img here …</div>
              </div>
         </div>
         <div class="clearfix"></div>
         <input type="hidden" name="cover" id="cover"/>
         <div class="input-group-btn">
             <button class="btn btn-blue" type="button" id="upload-btn">
                 <i class="fa fa-folder-open"></i>
                 <input id="upload-cover" name="upload-cover" multiple="multiple"
                        onchange="fileChange(this)" type="file"
                        accept="image/*"/>
             </button>
         </div>
         ...
    </form>
    ...
    
    1. 然后是定义一个fileChange方法来处理文件编码的工作,代码如下:
    function fileChange(obj){
            try{
                var file = obj.files[0];
                var reader = new FileReader();
                var fileName="";
                if(typeof(fileName) != "undefined"){
                    fileName = $(obj).val().split("\\").pop();
                }
                reader.onload = function(){
                    var img = new Image();
                    img.src = reader.result;
                    img.onload = function(){
                        var w = img.width,h = img.height;
                        var canvas = document.createElement("canvas");
                        var ctx = canvas.getContext("2d");
                        $(canvas).attr({
                            width:w,
                            height:h
                        });
                        ctx.drawImage(img,0,0,w,h);
                        var base64 = canvas.toDataURL("image/png",0.5);
                        var result = {
                            url:window.URL.createObjectURL(file),
                            base64:base64,
                            clearBase64:base64.substr(base64.indexOf(',')+1),
                          suffix:base64.substring(base64.indexOf(',')+1,base64.indexOf(';'))
                        };
                        $(".file-upload-zone-title").hide();
                        $(".file-upload-zone").empty();
                        $("#cover").val(result.base64);
                        $("<img src=\""+result.base64+"\" class=\"img img-responsive center-block\">").appendTo(".file-upload-zone");
                        $(".file-upload-zone").trigger("create");
                        $(".file-name").val(fileName);
                    }
                }
                reader.readAsDataURL(obj.files[0]);
            }catch(e){
                layer.msg("error");
            }
        };
    

关于这段代码的核心逻辑,其实与后端的解码过程刚好相反,这里不再赘述。

到现在,通过Base64编码方式同步上传文件的核心功能已经完成,在接下来的内容中,使用Spring Boot 2.0快速的演示本次分享的内容。

添加文章服务组件#文件上传

1. 添加文章的服务组件

接一开始的需求背景,图片信息属于文章对象的一个属性值,所以处理文件上传的逻辑后置到service中,在本次测试代码中,最终的文件存储采用的是七牛云的CDN服务,关于CDN部分的代码不进行展开,可以上传到本地,两者操作的对象都是MultipartFile,关于如何存储不是本次分享的重点。文章服务组件主要代码如下:

package com.ramostear.jfast.domain.service.impl;

import com.ramostear.jfast.common.ext.Translate;
import com.ramostear.jfast.domain.repo.ArticleRepo;
import com.ramostear.jfast.domain.service.ArticleService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
 * @author ramostear|谭朝红
 * @create-time 2019/3/19 0019-23:37
 * @modify by :
 * @since:
 */
@Service(value = "articleService")
@Transactional(readOnly = true)
public class ArticleServiceImpl implements ArticleService {
    @Autowired
    private ArticleRepo articleRepo;

    @Override
    @Transactional
    public void save(ArticleVo vo) {
        Article article = Translate.toArticle(vo);
        articleRepo.save(article);
    }

在ArticleService服务组件中,涉及到一个Translate类,它的作用主要是讲前端传输过来的ValueObject映射到POJO类中,同时将文件存储的逻辑也封装进去了,主要代码如下:

package com.ramostear.jfast.common.ext;

import com.ramostear.jfast.common.factory.CdnFactory;
import com.ramostear.jfast.common.factory.cdn.CdnRepository;
import com.ramostear.jfast.common.utils.Base64Converter;
import com.ramostear.jfast.domain.model.Article;
import com.ramostear.jfast.domain.vo.ArticleVo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.web.multipart.MultipartFile;

import java.util.Date;

/**
 * @author ramostear|谭朝红
 * @create-time 2019/3/18 0018-3:39
 * @modify by :
 * @since:
 */
public class Translate {

    private static CdnRepository cdnRepo = CdnFactory.builder(CdnFactory.CdnType.Qiniu);

    public static Article toArticle(ArticleVo vo){
        Article article = new Article();
        BeanUtils.copyProperties(vo,article);
        if(StringUtils.isNotBlank(vo.getCover())){
            MultipartFile file = Base64Converter.converter(vo.getCover());
            article.setCover(cdnRepo.save(file));
        }
        return article;
    }
}

此处由于使用的是七牛云的CDN服务,所以通过一个CND的工厂类获取一个CND仓储实例,用于将文件写入到仓储中,并回传一个文件访问地址。除了上述的方法,还可以调用file.transferTo()方法将文件写入到本地(应用服务器)磁盘中。

这里的CND工厂类实现细节由于篇幅原因不再展开。需要了解更多关于CDN SDK使用方法,可以在文章末尾给我留言。

2. 文章控制器

最后,定义一个控制器,提供给前端添加文章时进行调用,文章控制器主要工作是获得前端传入的文章信息,然后调用文章服务组件,完成添加文章工作。核心代码如下:

package com.ramostear.jfast.domain.controller;

@RestController
public class ArticleController{
    @Autowired
    ArticleService articleService;

    @Postmapping(value="/articles")
    public ResponseEntity<Object> createArticle(@RequestBody ArticleVo vo){
        try{
            articleService.save(vo);
            return new ResponseEntity<>("已经成功将文字写入数据库",HttpStatus.CREATED);
        }catch(Exception e){
            return new ResponseEntity<>(e.getMessage(),HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

}

结束语

本次分享只给出了核心部位的实现,其中涉及到的如CDN、HTML、JS等的知识没有展开,如果给你带来了困惑,可以在评论区给我留言,我们再一起讨论。再次感谢大家赏光拜读,谢谢~~~

关于如何快速调教Nginx的几点总结

关于如何快速调教Nginx的几点总结

关于如何快速调教Nginx的几点总结

关于Nginx的好与坏,我觉得没有必要去介绍了,在这里主要分享一下我在实际的项目部署中是如何快速的调教Nginx的。其中分享的源码大家可以作为模板代码,根据自身项目的实际情况,酌情使用。

这里简单的说一说我为什么要写这篇文章,网上有很多大而全的文章在介绍Nginx是什么,如何入门等等,玩了很多的文字游戏,反正我接触Nginx的时候,去查阅文档给我的是这种感觉,大而全,但是很乱。这里我要讲的不是Nginx的理论知识,而是一些能够快速的应用到项目中的实际技巧。废话就说这么多,开始本次分享的主体。

调教一:开启GZIP,提高页面加载速度

http:{
    ...

    gzip on;
    gzip_min_length 10;
    gzip_comp_level 4;
    gzip_disable "MSIE [1-10] \.";
    gzip_types text/plain appliaton/x-javascript text/css application/xml image/jpeg image/gif image/png image/svg+xml;

    ...
}
gzip on 开启gzip压缩功能
gzip_min_lenght 10 压缩临界值,大于10KB的文件才压缩
gzip_com_level 4 设置压缩级别[0-10],数字越大,压缩比越好,但消耗的时间越长
gzip_desable “MSIE [1-10].“ 对IE浏览器不采用压缩,[1-10]表示浏览器版本范围
gzip_types 需要进行文件压缩的类型,根据自身情况酌情添加

一般情况下,关于gzip的配置,设置以上几个参数就可以了

调教二:无www的域名跳转到带www的域名

server{
    listen 80;
    server_name http://youdomain.com;
    return 301 http://www.youdomain.com$request_uri;
}

针对自己的域名,配置一个全局的server,对裸域名的请求进行转发,注意要加上“$request_uri”

网上有关这个问题提供了另外一种解决办法,代码如下:

server{
    listen 80;
    server_name www.youdomain.com;
    if ( $host !='www.youdomain.com'){
        rewrite ^/(.*)$ http://www.youdomain.com/$1 permanent;
    }
    rewrite ^/(.*)$ http://$host$1 permanent;
}

我在自己的项目中使用第二种方式进行配置,貌似没有生效,所以改为了第一种配置方式

调教三:配置https

关于如何配置server(http)这里不再介绍,网上相关文档很多,这里主要分享如何在Nginx中配置HTTPS,配置代码如下:

server{
    listen 443 ssl;
    server_name www.youdomain.com;
    access_log logs/com_youdomain_logs.log;

    ssl_certificate  c:/sslfile/cert.crt;
    ssl_certificate_key c:/sslfile/cert.key;

    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout 5m;

    location /{
        proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header    Host  $host;
        proxy_set_header    X-Forwarded-Proto   https;
        proxy_set_header    X-real-IP  $remote_addr;
        proxy_set_header    X-Forwarded-proto  $scheme;            
        proxy_connect_timeout   240;
        proxy_send_timeout    240;
        proxy_read_timeout    240;
        proxy_pass    http://localhost:8080;
        proxy_redirect ~^http://([^:]+)(:\d+)?(.*)$ https://$1$3;    
    }
}

这里需要注意几个地方:

  • 监听的端口由原来的80或者其他(通常是80端口)改为 443 ssl 。
  • ssl_certificate配置HTTPS证书放置的路径,ssl_certificate_key 放置HTTPS证书的秘钥路径。
  • ssl_session_cache配置HTTPS的缓存,ssl_session_timeout配置HTTPS缓存的生命周期。
  • 在location配置中,proxy_set_header部分的代码是一个固定用法,不进行介绍。
  • proxy_connect_timeout,proxy_send_timeout和proxy_read_timeout主要配置在HTTPS下建立请求连接、发送数据和读取数据的时间上线(超时处理)
  • proxy_pass设置Nginx需要代理的请求对象,如http://localhost:8080 ,这里需要web容器配置,在接下来会单独介绍
  • proxy_redirect设置代理后的请求转发重定向:~^http://([^:]+)(:\d+)?(.*)$ https://$1$3; 将http请求重定向到https上。

要实现https加密请求,还需要web容器的配合,在这里以Apache Tomcat配置为例进行介绍。

调教三:开启tomcat对https请求的支持

在上一小节中,我们对server的代理做了如下的配置:

server{
    ...
    location /{
        ...
        proxy_pass http://localhost:8080;
        ...
    }
    ...
}

首先,我们需要将tomcat的连接器(Connector)的端口设置为8080,将转发重定向的端口(redirectPort)和代理端口(proxyPort)设置为443。具体的配置代码如下:

...
<Connector prot="8080" protocol="HTTP/1.1" connectionTimeout="20000"
           redirectPort="443" proxyPort="443"/>
...

然后,需要在Host配置中设置remoteIpHeaderprotocolHeaderprotocolHeaderHttpsValue这三个属性的值,详细配置如下:

...

    <Host name="localhost" appBase="webapps" ....>
        <Value className="org.apache.catalina.values.RemoteIpValue"
               remoteIpHeader="X-Forwarded-For"
               protocolHeader="X-Forwarded-Proto"
               protocolHeaderHttpsValue="https"/>
        ...
        ...
        <Context docBase="" path="" reloadable="false"></Context>

    </Host>

...

以上就是Nginx+tomcat的组合方式开启https请求的调教过程。

结束语

本文是我在实际项目开发过程中认为比较常用其重要的几个调教点技巧,希望本次分享能够帮到你。此次文章主要分享关于Nginx小而精的一些常用配置技巧,更多的配置如分布式下一服多实例的配置我会单独些一篇文章进行分享,今天的内容就到这里结束了;再次感谢你的拜读,拜拜~~

使用Ehcache三步搞定Spring Boot 缓存?

使用Ehcache三步搞定Spring Boot 缓存?

三步搞定Spring Boot 缓存

本次内容主要介绍基于Ehcache 3.0来快速实现Spring Boot应用程序的数据缓存功能。在Spring Boot应用程序中,我们可以通过Spring Caching来快速搞定数据缓存。接下来我们将介绍如何在三步之内搞定Spring Boot缓存。

1. 创建一个Spring Boot工程并添加Maven依赖

你所创建的Spring Boot应用程序的maven依赖文件至少应该是下面的样子:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ramostear</groupId>
    <artifactId>cache</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>cache</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.cache</groupId>
            <artifactId>cache-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

依赖说明:

  • spring-boot-starter-cache为Spring Boot应用程序提供缓存支持
  • ehcache提供了Ehcache的缓存实现
  • cache-api 提供了基于JSR-107的缓存规范

2. 配置Ehcache缓存

现在,需要告诉Spring Boot去哪里找缓存配置文件,这需要在Spring Boot配置文件中进行设置:

spring.cache.jcache.config=classpath:ehcache.xml

然后使用@EnableCaching注解开启Spring Boot应用程序缓存功能,你可以在应用主类中进行操作:

package com.ramostear.cache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class CacheApplication {

    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }
}

接下来,需要创建一个ehcache的配置文件,该文件放置在类路径下,如resources目录下:

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.ehcache.org/v3"
        xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
        xsi:schemaLocation="
            http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
            http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
    <service>
        <jsr107:defaults enable-statistics="true"/>
    </service>

    <cache alias="person">
        <key-type>java.lang.Long</key-type>
        <value-type>com.ramostear.cache.entity.Person</value-type>
        <expiry>
            <ttl unit="minutes">1</ttl>
        </expiry>
        <listeners>
            <listener>
                <class>com.ramostear.cache.config.PersonCacheEventLogger</class>
                <event-firing-mode>ASYNCHRONOUS</event-firing-mode>
                <event-ordering-mode>UNORDERED</event-ordering-mode>
                <events-to-fire-on>CREATED</events-to-fire-on>
                <events-to-fire-on>UPDATED</events-to-fire-on>
                <events-to-fire-on>EXPIRED</events-to-fire-on>
                <events-to-fire-on>REMOVED</events-to-fire-on>
                <events-to-fire-on>EVICTED</events-to-fire-on>
            </listener>
        </listeners>
        <resources>
                <heap unit="entries">2000</heap>
                <offheap unit="MB">100</offheap>
        </resources>
    </cache>
</config>

最后,还需要定义个缓存事件监听器,用于记录系统操作缓存数据的情况,最快的方法是实现CacheEventListener接口:

package com.ramostear.cache.config;

import org.ehcache.event.CacheEvent;
import org.ehcache.event.CacheEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author ramostear
 * @create-time 2019/4/7 0007-0:48
 * @modify by :
 * @since:
 */
public class PersonCacheEventLogger implements CacheEventListener<Object,Object>{

    private static final Logger logger = LoggerFactory.getLogger(PersonCacheEventLogger.class);

    @Override
    public void onEvent(CacheEvent cacheEvent) {
        logger.info("person caching event {} {} {} {}",
                cacheEvent.getType(),
                cacheEvent.getKey(),
                cacheEvent.getOldValue(),
                cacheEvent.getNewValue());
    }
}

3. 使用@Cacheable注解对方法进行注释

要让Spring Boot能够缓存我们的数据,还需要使用@Cacheable注解对业务方法进行注释,告诉Spring Boot该方法中产生的数据需要加入到缓存中:

package com.ramostear.cache.service;

import com.ramostear.cache.entity.Person;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**
 * @author ramostear
 * @create-time 2019/4/7 0007-0:51
 * @modify by :
 * @since:
 */
@Service(value = "personService")
public class PersonService {

    @Cacheable(cacheNames = "person",key = "#id")
    public Person getPerson(Long id){
        Person person = new Person(id,"ramostear","ramostear@163.com");
        return person;
    }
}

通过以上三个步骤,我们就完成了Spring Boot的缓存功能。接下来,我们将测试一下缓存的实际情况。

4. 缓存测试

为了测试我们的应用程序,创建一个简单的Restful端点,它将调用PersonService返回一个Person对象:

package com.ramostear.cache.controller;

import com.ramostear.cache.entity.Person;
import com.ramostear.cache.service.PersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * @author ramostear
 * @create-time 2019/4/7 0007-0:54
 * @modify by :
 * @since:
 */
@RestController
@RequestMapping("/persons")
public class PersonController {

    @Autowired
    private PersonService personService;

    @GetMapping("/{id}")
    public ResponseEntity<Person> person(@PathVariable(value = "id") Long id){
        return new ResponseEntity<>(personService.getPerson(id), HttpStatus.OK);
    }
}

Person是一个简单的POJO类:

package com.ramostear.cache.entity;


import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.io.Serializable;

/**
 * @author ramostear
 * @create-time 2019/4/7 0007-0:45
 * @modify by :
 * @since:
 */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable{

    private Long id;

    private String username;

    private String email;
}

以上准备工作都完成后,让我们编译并运行应用程序。项目成功启动后,使用浏览器打开:http://localhost:8080/persons/1 ,你将在浏览器页面中看到如下的信息:

{"id":1,"username":"ramostear","email":"ramostear@163.com"}

此时在观察控制台输出的日志信息:

2019-04-07 01:08:01.001  INFO 6704 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms
2019-04-07 01:08:01.054  INFO 6704 --- [e [_default_]-0] c.r.cache.config.PersonCacheEventLogger  : person caching event CREATED 1 null com.ramostear.cache.entity.Person@ba8a729

由于我们是第一次请求API,没有任何缓存数据。因此,Ehcache创建了一条缓存数据,可以通过CREATED看一了解到。

我们在ehcache.xml文件中将缓存过期时间设置成了1分钟(1),因此在一分钟之内我们刷新浏览器,不会看到有新的日志输出,一分钟之后,缓存过期,我们再次刷新浏览器,将看到如下的日志输出:

2019-04-07 01:09:28.612  INFO 6704 --- [e [_default_]-1] c.r.cache.config.PersonCacheEventLogger  : person caching event EXPIRED 1 com.ramostear.cache.entity.Person@a9f3c57 null
2019-04-07 01:09:28.612  INFO 6704 --- [e [_default_]-1] c.r.cache.config.PersonCacheEventLogger  : person caching event CREATED 1 null com.ramostear.cache.entity.Person@416900ce

第一条日志提示缓存已经过期,第二条日志提示Ehcache重新创建了一条缓存数据。

结束语

在本次案例中,通过简单的三个步骤,讲解了基于Ehcache的Spring Boot应用程序缓存实现。文章内容重在缓存实现的基本步骤与方法,简化了具体的业务代码,有兴趣的朋友可以自行扩展,期间遇到问题也可以随时与我联系。

使用Spring Boot 2.0 十分钟构建Web应用程序

使用Spring Boot 2.0 十分钟构建Web应用程序

Spring Boot 2.0 :快速构建Web应用程序

1. 简介

Spring Boot 节约了我们对Spring Framework的学习和使用成本,它能够让我们以更短的时间,更高的效率去构建一个Web应用程序。本教程是Spring Boot 2.0系列教程的一个开端,我将在此文章中介绍一些Spring Boot 2.0的核心配置、前端应用、快速数据操作以及异常处理。

2. 配置

首先,我们使用Spring Initializr为我们的项目构建出基本的工程目录结构。

生成的项目将依赖于spring-boot-starter-parent:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
    <relativePath/>
</parent>

一开始的项目依赖关系很简单,pom文件中添加的核心依赖如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

大多数的Spring库都可以通过简单的配置相关的starters导入到我们的项目中。

3. 应用程序配置

接下来,我们将为我们的应用程序配置一个简单的主类(主类的配置Spring Initializer已经为我们完成):

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

说明:@SpringBootApplication 是一个组合注解,它等同于@Configuration@EnableAutoConfiguration@ComponentScan 三个注解的组合使用

最后,我们在定义一个简单的application.properties文件,设置一个简单的属性和值:

server.port = 8888

server.port = 8888 将把Spring Boot 设置的服务器端口从默认的8080更改为8888;其他属性的设置可参考Spring Boot 属性设置

4. MVC视图

在本次教程中,我们将使用Thymeleaf作为前端的模板,你也可以使用诸如Freemarker等其他的模板引擎。

首先,我们需要在pom文件中添加Thymeleaf的依赖,将spring-boot-starter-thymeleaf依赖项添加到pom.xml中:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

通过上述配置,Thymeleaf已经引入到项目中,我们只需要在application.properties中简单的配置即可使用它:

spring.thymeleaf.cache = false
spring.thymeleaf.enabled = true
spring.thymeleaf.prefix = classpath:/templates/
spring.thymeleaf.suffix = .html
spring.application.name = Building Web Application base on Spring Boot 2.0

接下来,我们将定义一个简单的控制器和一个前端页面,主页控制器如下:

@Controller
public class HomeController{
    @Value("${spring.application.name}")
    String appName;

    @GetMapping("/")
    public String home(Model model){
        model.addAttribute("appName",appName);
        return "home";
    }
}

最后,创建一个home.html页面:

<html>
    <head>
        <title>Home Page</title>
    </head>
    <body>
        <h1>
            Spring Boot 2.0 Tutorial
        </h1>
        <p>
            Server message: <span th:text="${appName}">default appName</span>
        </p>
    </body>
</html>

5. 应用安全

接下来,我们将使用Spring Security为我们的应用程序增加安全访问控制,首先是添加相关的依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

一旦我们导入并开启了Spirng Security,Spring Security将使用默认的httpBasic或者formLogin策略对我们的应用程序进行安全检查。也就是说默认情况下,我们应用中的所有端点都将被Spring Security进行安全保护,因此,我们需要通过扩展Spring Security的WebSecurityConfigurerAdapter类来自定义我们自己的自定义安全控制配置项:

@Configuration
@EnableWebSecurity
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter{
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests()
            .anyRequest()
            .permitAll()
            .and.csrf().disabled();
    }
}

通过上述的配置,在我们的应用中,所有端点的访问将不受任何限制。

Spring Security的内容不仅仅只是文中提到的这些,更多的内容可自行研究

6. 持久化

首先,让我们从定义数据模型开始,数据模型是持久化操作的基础。一个简单的用户实体:

@Entity
public class User{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(nullable = false,unique = true)
    private String username;

    @Column(nullable = false)
    private String address;
}

接下来,我们使用Spring Data来定义用户的存储库:

public interface UserRepository extends CrudRepository<User,Long>{
    List<User> findByUsername(String username);
}

最后,我们需要配置我们的持久化类:

@EnableJpaRepositories("com.ramostear.persistence.repo")
@EntityScan("com.ramostear.persistence.model")
@SpringBootApplication
public class Application{
    ...
}

相关注解说明:

为了快速的演示项目,我们使用了H2内存数据库,以减少项目在运行过程中所依赖的外部系统。一旦我们引入了H2的依赖项,Spring Boot会自动检测并设置我们的持久化关系,我们只需要在application.xml中配置数据源属性:

spring.datasource.driver-class-name = org.h2.Driver
spring.datasource.url = jdbc:h2:mem:bootapp;DB_CLOSE_DELAY=-1
spring.datasource.username = sa
spring.datasource.password =

7. Web层和控制器

在本小节中,我们将定义一个简单的控制器 —UserController来完成Web层的操作。我们将实现基本的CRUD操作,并通过REST 的方式来暴露User相关资源:

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping
    public Iterable<User> findAll(){
        return userRepository.findAll();
    }

    @GetMapping("/username/{username}")
    public List<User> findByUsername(@PathVariable(name = "username")String username){
        return userRepository.findAllByUsername(username);
    }

    @GetMapping("/{id}")
    public User findOne(@PathVariable Long id){
        return userRepository.findById(id)
                .orElseThrow(UserNotFoundException::new);
    }


    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User create(@RequestBody User user){
        User u = userRepository.save(user);
        return u;
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id){
        userRepository.findById(id)
                .orElseThrow(UserNotFoundException::new);
        userRepository.deleteById(id);
    }

    @PutMapping("/{id}")
    public User updateUser(@RequestBody User user,@PathVariable Long id){
        if(!id.equals(user.getId())){
            throw new UserIdMismatchException();
        }
        userRepository.findById(id)
                .orElseThrow(UserNotFoundException::new);
        return userRepository.save(user);
    }
}

鉴于文章篇幅问题,我们不再去定义前端页面,而是使用@RestController注解,将控制器定义为一个API控制器。这里的@RestController相当于@Controller@ResponseBody两个注解的组合。

8 . 异常处理

现在,Web应用的核心功能已经准备就绪,接下来我们将使用@ControllerAdvice注解来集中处理应用中可能出现的异常信息。

首先,我们需要定义一个自定义的异常类:

public class UserNotFoundException extends RuntimeException{
    public UserNotFoundException(String message,Throwable cause){
        super(message,cause);
    }
    ....
}

然后,使用@ControllerAdvice定义我们的异常信息统一处理类:

@ControllerAdvice
public RestExceptionHandler extends ResponseEntityExceptionHandler{

    @ExceptionHandler({UserNotFoundException.class})
    protected ResponseEntity<Object> handleNotFound(Exception ex,WebRequest request){
        return handleExceptionInternal(ex,"User not found",
                                       new HttpHeaders(),
                                       HttpStatus.NOT_FOUND,request);
    }

    @ExceptionHandler({UserIdMIsmatchException.class,
                      ConstraintViolationException.class,
                      DataIntegrityViolationException.class})
    public ResponseEntity<Object> handleBadRequest(Exception ex,WebRequest request){
        return handleExceptionInternal(ex,ex.getLocalizedMessage(),
                                      new HttpHeaders(),HttpStatus.BAD_REQUEST,request);
    }
}

默认情况下,Spring Boot 已提供了错误信息页面映射。我们也可以自定义一个error.html页面来定义其视图:

<html>
    <head>
        <title>Error Page</title>
    </head>
    <body>
        <h1>
            Error infomation !
        </h1>
        <b>
            [<span th:text="${status}">status</span>]
            <span th:text="${error}">error</span>
        </b>
        <p th:text="${message}">
            message
        </p>
    </body>
</html>

与设置Spring Boot中其他属性一样,我们可以在application.properties文件中覆盖默认的映射路径:

server.error.path = /error

9 . 应用测试

最后,我们将对我们定义的用户访问接口进行测试。我们将使用@SpringBootTest注解来加载应用程序的上下文:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class},webEnvironment = WebEnvironment.DEFINED_PORT)
public class UserAPITest{
    private static final String API_ROOT_URL = "http://localhost:8888/api/users";

     private User createRandomUser(){
        final User user = new User();
        user.setUsername(RandomStringUtils.randomAlphabetic(10));
        user.setAddress(RandomStringUtils.randomAlphabetic(15));
        return user;
    }

    private String  createUserAsUri(User user){
        final Response response = RestAssured.given()
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .body(user)
                .post(API_ROOT_URL);
        return API_ROOT_URL+"/"+response.jsonPath().get("id");
    }

}

首先,我们通过不同使用不同条件来查找用户:

@Test
    public void whenGetAllUsers_thenOk(){
        final Response response = RestAssured.get(API_ROOT_URL);
        Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());
    }

    @Test
    public void whenGetUsersByUsername_thenOk(){
        final User user = createRandomUser();
        createUserAsUri(user);
        final Response response = RestAssured.get(API_ROOT_URL+"/username/"+user.getUsername());
        Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());
       Assert.assertTrue(response.as(List.class).size() >0);
    }

    @Test
    public void whenGetCreatedUserById_thenOk(){
        final User user = createRandomUser();
        final String location = createUserAsUri(user);
        System.out.println(location);
        final Response response = RestAssured.get(location);
        Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());
        Assert.assertEquals(user.getUsername(),response.jsonPath().get("username"));
    }

    @Test
    public void whenGetNotExistUserById_thenNotFound(){
        final Response response = RestAssured.get(API_ROOT_URL+"/"+RandomStringUtils.randomNumeric(4));
        Assert.assertEquals(HttpStatus.NOT_FOUND.value(),response.getStatusCode());
    }

接下来,我们尝试测试新增一个用户:

    @Test
     public void whenCreateNewUser_thenCreated(){
        final User user = createRandomUser();
        final Response response = RestAssured.given()
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .body(user)
                .post(API_ROOT_URL);
                Assert.assertEquals(HttpStatus.CREATED.value(),
                            response.getStatusCode());
    }

    @Test
    public void whenInvalidUserByUsernameNull_thenError(){
        final User user = createRandomUser();
        user.setUsername(null);
        final Response response = RestAssured.given()
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .body(user)
                .put(API_ROOT_URL);
                    Assert.assertEquals(HttpStatus.METHOD_NOT_ALLOWED.value(),
                                    response.getStatusCode());
    }

然后尝试更新一个用户的信息:

@Test
    public void whenUpdateCreatedUser_thenUpdated(){
        final User user = createRandomUser();
        final String location = createUserAsUri(user);
        user.setId(Long.parseLong(location.split("api/users/")[1]));
        user.setUsername("newUsername");
        Response response = RestAssured.given()
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .body(user)
                .put(location);
        Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());
        response = RestAssured.get(location);
        Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());
        Assert.assertEquals("newUsername",response.jsonPath().get("username"));
    }

最后,我们试着去移除一个用户:

@Test
public void whenDeleteCreatedUser_thenOk(){
    final User user = createRandomUser();
    final String location = createUserAsUri(user);
    Response response = RestAssured.delete(location);
    Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());

    response = RestAssured.get(location);
    Assert.assertEquals(HttpStatus.NOT_FOUND.value(),response.getStatusCode());
}

测试结果:

10 . 总结

此教程是Spring Boot 2.0 系列教程的快速预览篇,通过一个简单的Demo快速的对Spring Boot有一个初步的认识。由于篇幅有限,不可能涵盖所有的技术细节,Spring Boot 2.0的核心技术教程将在其他教程中一一进行介绍。你可以通过Github网站获取本教程的全部源码:https://github.com/ramostear/spring-boot-tutorial-part1

如何在生产环境中重启Spring Boot应用?

如何在生产环境中重启Spring Boot应用?

通过HTTP重启Spring Boot应用程序

需求背景

在一个很奇葩的需求下,要求在客户端动态修改Spring Boot配置文件中的属性,例如端口号、应用名称、数据库连接信息等,然后通过一个Http请求重启Spring Boot程序。这个需求类似于操作系统更新配置后需要进行重启系统才能生效的应用场景。

动态配置系统并更新生效是应用的一种通用性需求,实现的方式也有很多种。例如监听配置文件变化、使用配置中心等等。网络上也有很多类似的教程存在,但大多数都是在开发阶段,借助Spring Boot DevTools插件实现应用程序的重启,或者是使用spring-boot-starter-actuator和spring-cloud-starter-config来提供端点(Endpoint)的刷新。

第一种方式无法在生产环境中使用(不考虑),第二种方式需要引入Spring Cloud相关内容,这无疑是杀鸡用了宰牛刀。

接下来,我将尝试采用另外一种方式实现HTTP请求重启Spring Boot应用程序这个怪异的需求。

尝试思路

重启Spring Boot应用程序的关键步骤是对主类中SpringApplication.run(Application.class,args);方法返回值的处理。SpringApplication#run()方法将会返回一个ConfigurableApplicationContext类型对象,通过查看官方文档可以看到,ConfigurableApplicationContext接口类中定义了一个close()方法,可以用来关闭当前应用的上下文:

package org.springframework.context;

import java.io.Closeable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.ProtocolResolver;
import org.springframework.lang.Nullable;

public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {
void close();

}

继续看官方源码,AbstractApplicationContext类中实现close()方法,下面是实现类中的方法摘要:

public void close() {
        Object var1 = this.startupShutdownMonitor;
        synchronized(this.startupShutdownMonitor) {
            this.doClose();
            if (this.shutdownHook != null) {
                try {
                    Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
                } catch (IllegalStateException var4) {
                    ;
                }
            }

        }
    }

#close()方法将会调用#doClose()方法,我们再来看看#doClose()方法做了哪些操作,下面是doClose()方法的摘要:

protected void doClose() {
        if (this.active.get() && this.closed.compareAndSet(false, true)) {

            ...

            LiveBeansView.unregisterApplicationContext(this);

            ...

            this.destroyBeans();
            this.closeBeanFactory();
            this.onClose();
            if (this.earlyApplicationListeners != null) {
                this.applicationListeners.clear();
                this.applicationListeners.addAll(this.earlyApplicationListeners);
            }

            this.active.set(false);
        }

    }

#doClose()方法中,首先将应用上下文从注册表中清除掉,然后是销毁Bean工厂中的Beans,紧接着关闭Bean工厂。

官方文档看到这里,就产生了解决一个结局重启应用应用程序的大胆猜想。在应用程序的main()方法中,我们可以使用一个临时变量来存放SpringApplication.run()返回的ConfigurableApplicationContext对象,当我们完成对Spring Boot应用程序中属性的设置后,调用ConfigurableApplicationContext#close()方法,最后再调用SpringApplication.run()方法重新给ConfigurableApplicationContext对象进行赋值已达到重启的效果。

现在,我们再来看一下SpringApplication.run()方法中是如何重新创建ConfigurableApplicationContext对象的。在SpringApplication类中,run()方法会调用createApplicationContext()方法来创建一个ApplicationContext对象:

protected ConfigurableApplicationContext createApplicationContext() {
        Class<?> contextClass = this.applicationContextClass;
        if (contextClass == null) {
            try {
                switch(this.webApplicationType) {
                case SERVLET:
                    contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext");
                    break;
                case REACTIVE:
                    contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext");
                    break;
                default:
                    contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext");
                }
            } catch (ClassNotFoundException var3) {
                throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
            }
        }

        return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);
    }

createApplicationContext()方法会根据WebApplicationType类型来创建ApplicationContext对象。在WebApplicationType中定义了三种种类型:NONESERVLETREACTIVE。通常情况下,将会创建servlet类型的ApplicationContext对象。

接下来,我将以一个简单的Spring Boot工程来验证上述的猜想是否能够达到重启Spring Boot应用程序的需求。

编码实现

首先,在application.properties文件中加入如下的配置信息,为动态修改配置信息提供数据:

spring.application.name= SPRING-BOOT-APPLICATION

接下来,在Spring Boot主类中定义两个私有变量,用于存放main()方法的参数和SpringApplication.run()方法返回的值。下面的代码给出了主类的示例:

public class ExampleRestartApplication {

    @Value ( "${spring.application.name}" )
    String appName;

    private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class );

    private static String[] args;
    private static ConfigurableApplicationContext context;

    public static void main(String[] args) {
        ExampleRestartApplication.args = args;
        ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args);
    }
}

最后,直接在主类中定义用于刷新并重启Spring Boot应用程序的端点(Endpoint),并使用@RestController注解对主类进行注释。

@GetMapping("/refresh")
public String restart(){
    logger.info ( "spring.application.name:"+appName);
    try {
        PropUtil.init ().write ( "spring.application.name","SPRING-DYNAMIC-SERVER" );
    } catch (IOException e) {
        e.printStackTrace ( );
    }

    ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue<> ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ());
    threadPool.execute (()->{
        context.close ();
        context = SpringApplication.run ( ExampleRestartApplication.class,args );
    } );
    threadPool.shutdown ();
    return "spring.application.name:"+appName;
}

说明:为了能够重新启动Spring Boot应用程序,需要将close()和run()方法放在一个独立的线程中执行。

为了验证Spring Boot应用程序在被修改重启有相关的属性有没有生效,再添加一个获取属性信息的端点,返回配置属性的信息。

@GetMapping("/info")
public String info(){
    logger.info ( "spring.application.name:"+appName);
    return appName;
}

完整的代码

下面给出了主类的全部代码:

package com.ramostear.application;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.concurrent.*;

/**
 * @author ramostear
 */
@SpringBootApplication
@RestController
public class ExampleRestartApplication {

    @Value ( "${spring.application.name}" )
    String appName;

    private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class );

    private static String[] args;
    private static ConfigurableApplicationContext context;

    public static void main(String[] args) {
        ExampleRestartApplication.args = args;
        ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args);
    }

    @GetMapping("/refresh")
    public String restart(){
        logger.info ( "spring.application.name:"+appName);
        try {
            PropUtil.init ().write ( "spring.application.name","SPRING-DYNAMIC-SERVER" );
        } catch (IOException e) {
            e.printStackTrace ( );
        }

        ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue<> ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ());
        threadPool.execute (()->{
            context.close ();
            context = SpringApplication.run ( ExampleRestartApplication.class,args );
        } );
        threadPool.shutdown ();
        return "spring.application.name:"+appName;
    }

    @GetMapping("/info")
    public String info(){
        logger.info ( "spring.application.name:"+appName);
        return appName;
    }
}

接下来,运行Spring Boot程序,下面是应用程序启动成功后控制台输出的日志信息:

[2019-03-12T19:05:53.053z][org.springframework.scheduling.concurrent.ExecutorConfigurationSupport][main][171][INFO ] Initializing ExecutorService 'applicationTaskExecutor'
[2019-03-12T19:05:53.053z][org.apache.juli.logging.DirectJDKLog][main][173][INFO ] Starting ProtocolHandler ["http-nio-8080"]
[2019-03-12T19:05:53.053z][org.springframework.boot.web.embedded.tomcat.TomcatWebServer][main][204][INFO ] Tomcat started on port(s): 8080 (http) with context path ''
[2019-03-12T19:05:53.053z][org.springframework.boot.StartupInfoLogger][main][59][INFO ] Started ExampleRestartApplication in 1.587 seconds (JVM running for 2.058)

在测试修改系统配置并重启之前,使用Postman测试工具访问:http://localhost:8080/info ,查看一下返回的信息:

成功返回SPRING-BOOT-APPLICATION提示信息。

然后,访问:http://localhost:8080/refresh ,设置应用应用程序spring.application.name的值为SPRING-DYNAMIC-SERVER,观察控制台输出的日志信息:

可以看到,Spring Boot应用程序已经重新启动成功,最后,在此访问:http://localhost:8080/info ,验证之前的修改是否生效:

请求成功返回了SPRING-DYNAMIC-SERVER信息,最后在看一眼application.properties文件中的配置信息是否真的被修改了:

配置文件的属性也被成功的修改,证明之前的猜想验证成功了。

本次内容所描述的方法不适用于以JAR文件启动的Spring Boot应用程序,以WAR包的方式启动应用程序亲测可用。┏ (^ω^)=☞目前该药方副作用未知,如有大牛路过,还望留步指点迷津,不胜感激。

结束语

本次内容记录了自己验证HTTP请求重启Spring Boot应用程序试验的一次经历,文章中所涉及到的内容仅代表个人的一些观点和不成熟的想法,并未将此方法应用到实际的项目中去,如因引用本次内容中的方法应用到实际生产开发工作中所带来的风险,需引用者自行承担因风险带来的后遗症(๑→ܫ←)——此药方还有待商榷(O_o)(o_O)。

Spring Boot Admin Client介绍

Spring Boot Admin Client介绍

Spring Boot(十八)— Admin Client

在上一章节中,我们学习了如何构建Spring Boot Admin Server应用程序。在本章节中,将学习如何让我们的应用程序被Admin Server所管理。相对于Admin Server来说,需要被管理的应用程序称为Admin Client

添加依赖

admin client需要在Maven build文件中添加如下的两个依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-client</artifactId>
</dependency>

配置Admin Client

在application.properties文件中配置应用程序的启动端口、应用名称和Admin Server的URL地址:

server.port=8081
spring.application.name=Admin-Client-1
spring.boot.admin.client.url=http://localhost:9091
management.endpoints.web.exposure.include=*

打包运行

使用下面的maven命令对应用程序进行打包:

mvn clean install

打包成功后,使用入下的命令运行JAR文件:

java -jar JARFILE

最后,我们还需要将Admin Server启动。

接下来,在web浏览器中输入URL:http://localhost:9091 访问Spring Boot Admin Server:

Admin Server index

应用监控详情页:

应用监控详情页

JVM监控页面:

JVM监控页面

应用实例信息页面:

应用实例信息页面

日志信息页面:

日志信息页面

Spring Boot Admin Server的应用

Spring Boot Admin Server的应用

Spring Boot(十七)— Admin Server

在上一章节中,我们学习了如何使用Spring Boot Actuator来管理应用程序。但是,当我们有多个应用程序需要管理时,使用Actuator来管理这些应用将变得很吃力。因为,当你有N个应用程序需要管理时,每个应用程序都有自己独立的端点信息,维护这些独立的端点信息将是一件可怕的事情。

面对这样的窘境,Spring Boot为我们提供了另外一个神器——Spring Boot Admin Server。它可以统一的在一个地方来管理所有的端点信息,与此同时,CodeCentric团队还提供了一套基于VUE的管理界面,方便我们对各个应用程序进行管理。

在本章节中,我们将使用Spring Boot快熟的构建起一个Admin Server应用程序,在下一个章节中,我们将讲解Admin Client如何与Admin Server一起工作。

构建Spring Boot Admin Server

为了能够构建Admin Server应用程序,我们需要在Maven build文件中引入以下的两个依赖项:

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-server-ui</artifactId>
</dependency>

其中,spring-boot-admin-server-ui为我们提供了一套漂亮的应用管理界面。

开启对Admin Server的支持

使用@EnableAdminServer注解对应用程序的主类进行注释。@EnableAdminServer注解可以让你的应用程序具备管理其他应用程序端点信息的能力。

package com.ramostear.application;

import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableAdminServer
public class AdminServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(AdminServerApplication.class, args);
    }

}

配置应用程序

现在,在application.properties文件中定义如下的几个配置:

server.port=9091
spring.application.name= Admin-Server
spring.application.admin.enabled=true

第一个参数时设置应用程序的启动端口号为9091,第二个参数时设置应用的名称为“Admin-Server”,最后一个参数时开启Admin Server服务。

Maven build 文件清单

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ramostear</groupId>
    <artifactId>admin-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>admin-server</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot-admin.version>2.1.3</spring-boot-admin.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
        </dependency>
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-server-ui</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>de.codecentric</groupId>
                <artifactId>spring-boot-admin-dependencies</artifactId>
                <version>${spring-boot-admin.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

接下来,使用下面的Maven命令对项目进行打包:

mvn clean install

打包成功后,使用下面的命令运行JAR文件:

java -jar JARFILE

现在,应用程序已经在Tomcat上启动成功,端口号为:9091 。

2019-03-12 01:44:55.289  INFO 18044 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : 
Exposing 2 endpoint(s) beneath base path '/actuator'
2019-03-12 01:44:55.803  INFO 18044 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : 
Netty started on port(s): 9091
2019-03-12 01:44:55.806  INFO 18044 --- [           main] c.r.application.AdminServerApplication   : 
Started AdminServerApplication in 2.81 seconds (JVM running for 3.933)

访问Admin Server

现在,打开Web浏览器,输入URL:http://localhost:9091/ ,然后查看服务管理的界面。

Spring Boot Admin Server UI

在下一章节中,我们将学习Admin Client的相关知识。