博客

  • 批处理框架 Spring Batch 这么强,你会用吗?

    快乐分享,Java干货及时送达👇

    文章来源:https://blog.csdn.net/topdeveloperr/article/details/84337956

    spring batch简介


    spring batch是spring提供的一个数据处理框架。企业域中的许多应用程序需要批量处理才能在关键任务环境中执行业务操作。这些业务运营包括:

    • 无需用户交互即可最有效地处理大量信息的自动化,复杂处理。这些操作通常包括基于时间的事件(例如月末计算,通知或通信)。
    • 在非常大的数据集中重复处理复杂业务规则的定期应用(例如,保险利益确定或费率调整)。
    • 集成从内部和外部系统接收的信息,这些信息通常需要以事务方式格式化,验证和处理到记录系统中。批处理用于每天为企业处理数十亿的交易。

    Spring Batch是一个轻量级,全面的批处理框架,旨在开发对企业系统日常运营至关重要的强大批处理应用程序。Spring Batch构建了人们期望的Spring Framework特性(生产力,基于POJO的开发方法和一般易用性),同时使开发人员可以在必要时轻松访问和利用更高级的企业服务。Spring Batch不是一个schuedling的框架。

    Spring Batch提供了可重用的功能,这些功能对于处理大量的数据至关重要,包括记录/跟踪,事务管理,作业处理统计,作业重启,跳过和资源管理。它还提供更高级的技术服务和功能,通过优化和分区技术实现极高容量和高性能的批处理作业。Spring Batch可用于两种简单的用例(例如将文件读入数据库或运行存储过程)以及复杂的大量用例(例如在数据库之间移动大量数据,转换它等等) 上)。大批量批处理作业可以高度可扩展的方式利用该框架来处理大量信息。


    Spring Batch架构介绍


    一个典型的批处理应用程序大致如下:

    • 从数据库,文件或队列中读取大量记录。
    • 以某种方式处理数据。
    • 以修改之后的形式写回数据。

    其对应的示意图如下:

    spring batch的一个总体的架构如下:

    在spring batch中一个job可以定义很多的步骤step,在每一个step里面可以定义其专属的ItemReader用于读取数据,ItemProcesseor用于处理数据,ItemWriter用于写数据,而每一个定义的job则都在JobRepository里面,我们可以通过JobLauncher来启动某一个job。


    Spring Batch核心概念介绍



    下面是一些概念是Spring batch框架中的核心概念。

    1、什么是Job

    Job和Step是spring batch执行批处理任务最为核心的两个概念。

    其中Job是一个封装整个批处理过程的一个概念。Job在spring batch的体系当中只是一个最顶层的一个抽象概念,体现在代码当中则它只是一个最上层的接口,其代码如下:


    /**
     * Batch domain object representing a job. Job is an explicit abstraction
     * representing the configuration of a job specified by a developer. It should
     * be noted that restart policy is applied to the job as a whole and not to a
     * step.
     */

    public interface Job {

     String getName();


     boolean isRestartable();


     void execute(JobExecution execution);


     JobParametersIncrementer getJobParametersIncrementer();


     JobParametersValidator getJobParametersValidator();

    }

    在Job这个接口当中定义了五个方法,它的实现类主要有两种类型的job,一个是simplejob,另一个是flowjob。在spring batch当中,job是最顶层的抽象,除job之外我们还有JobInstance以及JobExecution这两个更加底层的抽象。

    一个job是我们运行的基本单位,它内部由step组成。job本质上可以看成step的一个容器。一个job可以按照指定的逻辑顺序组合step,并提供了我们给所有step设置相同属性的方法,例如一些事件监听,跳过策略。

    Spring Batch以SimpleJob类的形式提供了Job接口的默认简单实现,它在Job之上创建了一些标准功能。一个使用java config的例子代码如下:

    @Bean
    public Job footballJob() {
        return this.jobBuilderFactory.get("footballJob")
                         .start(playerLoad())
                         .next(gameLoad())
                         .next(playerSummarization())
                         .end()
                         .build();
    }

    这个配置的意思是:首先给这个job起了一个名字叫footballJob,接着指定了这个job的三个step,他们分别由方法,playerLoad,gameLoad, playerSummarization实现。

    2、什么是JobInstance

    我们在上文已经提到了JobInstance,他是Job的更加底层的一个抽象,他的定义如下:

    public interface JobInstance {
     /**
      * Get unique id for this JobInstance.
      * @return instance id
      */

     public long getInstanceId();
     /**
      * Get job name.
      * @return value of 'id' attribute from 
      */

     public String getJobName();
    }

    他的方法很简单,一个是返回Job的id,另一个是返回Job的名字。

    JobInstance指的是job运行当中,作业执行过程当中的概念。Instance本就是实例的意思。

    比如说现在有一个批处理的job,它的功能是在一天结束时执行行一次。我们假定这个批处理job的名字为’EndOfDay’。在这个情况下,那么每天就会有一个逻辑意义上的JobInstance, 而我们必须记录job的每次运行的情况。

    3、什么是JobParameters

    在上文当中我们提到了,同一个job每天运行一次的话,那么每天都有一个jobIntsance,但他们的job定义都是一样的,那么我们怎么来区别一个job的不同jobinstance了。不妨先做个猜想,虽然jobinstance的job定义一样,但是他们有的东西就不一样,例如运行时间。

    spring batch中提供的用来标识一个jobinstance的东西是:JobParameters。JobParameters对象包含一组用于启动批处理作业的参数,它可以在运行期间用于识别或甚至用作参考数据。我们假设的运行时间,就可以作为一个JobParameters。

    例如, 我们前面的’EndOfDay’的job现在已经有了两个实例,一个产生于1月1日,另一个产生于1月2日,那么我们就可以定义两个JobParameter对象:一个的参数是01-01, 另一个的参数是01-02。因此,识别一个JobInstance的方法可以定义为:

    因此,我么可以通过Jobparameter来操作正确的JobInstance

    4、什么是JobExecution

    JobExecution指的是单次尝试运行一个我们定义好的Job的代码层面的概念。job的一次执行可能以失败也可能成功。只有当执行成功完成时,给定的与执行相对应的JobInstance才也被视为完成。

    还是以前面描述的EndOfDay的job作为示例,假设第一次运行01-01-2019的JobInstance结果是失败。那么此时如果使用与第一次运行相同的Jobparameter参数(即01-01-2019)作业参数再次运行,那么就会创建一个对应于之前jobInstance的一个新的JobExecution实例,JobInstance仍然只有一个。

    JobExecution的接口定义如下:

    public interface JobExecution {
     /**
      * Get unique id for this JobExecution.
      * @return execution id
      */

     public long getExecutionId();
     /**
      * Get job name.
      * @return value of 'id' attribute from 
      */

     public String getJobName();
     /**
      * Get batch status of this execution.
      * @return batch status value.
      */

     public BatchStatus getBatchStatus();
     /**
      * Get time execution entered STARTED status.
      * @return date (time)
      */

     public Date getStartTime();
     /**
      * Get time execution entered end status: COMPLETED, STOPPED, FAILED
      * @return date (time)
      */

     public Date getEndTime();
     /**
      * Get execution exit status.
      * @return exit status.
      */

     public String getExitStatus();
     /**
      * Get time execution was created.
      * @return date (time)
      */

     public Date getCreateTime();
     /**
      * Get time execution was last updated updated.
      * @return date (time)
      */

     public Date getLastUpdatedTime();
     /**
      * Get job parameters for this execution.
      * @return job parameters
      */

     public Properties getJobParameters();

    }

    每一个方法的注释已经解释的很清楚,这里不再多做解释。只提一下BatchStatus,JobExecution当中提供了一个方法getBatchStatus用于获取一个job某一次特地执行的一个状态。BatchStatus是一个代表job状态的枚举类,其定义如下:

    public enum BatchStatus {STARTING, STARTED, STOPPING,
       STOPPED, FAILED, COMPLETED, ABANDONED }

    这些属性对于一个job的执行来说是非常关键的信息,并且spring batch会将他们持久到数据库当中. 在使用Spring batch的过程当中spring batch会自动创建一些表用于存储一些job相关的信息,用于存储JobExecution的表为batch_job_execution,下面是一个从数据库当中截图的实例:

    5、什么是Step

    每一个Step对象都封装了批处理作业的一个独立的阶段。事实上,每一个Job本质上都是由一个或多个步骤组成。每一个step包含定义和控制实际批处理所需的所有信息。任何特定的内容都由编写Job的开发人员自行决定。一个step可以非常简单也可以非常复杂。例如,一个step的功能是将文件中的数据加载到数据库中,那么基于现在spring batch的支持则几乎不需要写代码。更复杂的step可能具有复杂的业务逻辑,这些逻辑作为处理的一部分。与Job一样,Step具有与JobExecution类似的StepExecution,如下图所示:

    6、什么是StepExecution

    StepExecution表示一次执行Step, 每次运行一个Step时都会创建一个新的StepExecution,类似于JobExecution。但是,某个步骤可能由于其之前的步骤失败而无法执行。且仅当Step实际启动时才会创建StepExecution。

    一次step执行的实例由StepExecution类的对象表示。每个StepExecution都包含对其相应步骤的引用以及JobExecution和事务相关的数据,例如提交和回滚计数以及开始和结束时间。此外,每个步骤执行都包含一个ExecutionContext,其中包含开发人员需要在批处理运行中保留的任何数据,例如重新启动所需的统计信息或状态信息。下面是一个从数据库当中截图的实例:

    7、什么是ExecutionContext

    ExecutionContext即每一个StepExecution 的执行环境。它包含一系列的键值对。我们可以用如下代码获取ExecutionContext

    ExecutionContext ecStep = stepExecution.getExecutionContext();
    ExecutionContext ecJob = jobExecution.getExecutionContext();

    8、什么是JobRepository

    JobRepository是一个用于将上述job,step等概念进行持久化的一个类。它同时给Job和Step以及下文会提到的JobLauncher实现提供CRUD操作。首次启动Job时,将从repository中获取JobExecution,并且在执行批处理的过程中,StepExecution和JobExecution将被存储到repository当中。

    @EnableBatchProcessing注解可以为JobRepository提供自动配置。

    9、什么是JobLauncher

    JobLauncher这个接口的功能非常简单,它是用于启动指定了JobParameters的Job,为什么这里要强调指定了JobParameter,原因其实我们在前面已经提到了,jobparameter和job一起才能组成一次job的执行。下面是代码实例:

    public interface JobLauncher {

    public JobExecution run(Job job, JobParameters jobParameters)
                throws JobExecutionAlreadyRunningException, JobRestartException,
                       JobInstanceAlreadyCompleteException, JobParametersInvalidException
    ;
    }

    上面run方法实现的功能是根据传入的job以及jobparamaters从JobRepository获取一个JobExecution并执行Job。

    10、什么是Item Reader

    ItemReader是一个读数据的抽象,它的功能是为每一个Step提供数据输入。当ItemReader以及读完所有数据时,它会返回null来告诉后续操作数据已经读完。Spring Batch为ItemReader提供了非常多的有用的实现类,比如JdbcPagingItemReader,JdbcCursorItemReader等等。

    ItemReader支持的读入的数据源也是非常丰富的,包括各种类型的数据库,文件,数据流,等等。几乎涵盖了我们的所有场景。

    下面是一个JdbcPagingItemReader的例子代码:

    @Bean
    public JdbcPagingItemReader itemReader(DataSource dataSource, PagingQueryProvider queryProvider) {
            Map parameterValues = new HashMap();
            parameterValues.put("status""NEW");

            return new JdbcPagingItemReaderBuilder()
                                               .name("creditReader")
                                               .dataSource(dataSource)
                                               .queryProvider(queryProvider)
                                               .parameterValues(parameterValues)
                                               .rowMapper(customerCreditMapper())
                                               .pageSize(1000)
                                               .build();
    }

    @Bean
    public SqlPagingQueryProviderFactoryBean queryProvider() {
            SqlPagingQueryProviderFactoryBean provider = new SqlPagingQueryProviderFactoryBean();

            provider.setSelectClause("select id, name, credit");
            provider.setFromClause("from customer");
            provider.setWhereClause("where status=:status");
            provider.setSortKey("id");

            return provider;
    }

    JdbcPagingItemReader必须指定一个PagingQueryProvider,负责提供SQL查询语句来按分页返回数据。

    下面是一个JdbcCursorItemReader的例子代码:

     private JdbcCursorItemReader> buildItemReader(final DataSource dataSource, String tableName,
                String tenant) {

            JdbcCursorItemReader> itemReader = new JdbcCursorItemReader();
            itemReader.setDataSource(dataSource);
            itemReader.setSql("sql here");
            itemReader.setRowMapper(new RowMapper());
            return itemReader;
        }

    11、什么是Item Writer

    既然ItemReader是读数据的一个抽象,那么ItemWriter自然就是一个写数据的抽象,它是为每一个step提供数据写出的功能。写的单位是可以配置的,我们可以一次写一条数据,也可以一次写一个chunk的数据,关于chunk下文会有专门的介绍。ItemWriter对于读入的数据是不能做任何操作的。

    Spring Batch为ItemWriter也提供了非常多的有用的实现类,当然我们也可以去实现自己的writer功能。

    12、什么是Item Processor

    ItemProcessor对项目的业务逻辑处理的一个抽象, 当ItemReader读取到一条记录之后,ItemWriter还未写入这条记录之前,I我们可以借助temProcessor提供一个处理业务逻辑的功能,并对数据进行相应操作。如果我们在ItemProcessor发现一条数据不应该被写入,可以通过返回null来表示。ItemProcessor和ItemReader以及ItemWriter可以非常好的结合在一起工作,他们之间的数据传输也非常方便。我们直接使用即可。


    chunk 处理流程


    spring batch提供了让我们按照chunk处理数据的能力,一个chunk的示意图如下:

    它的意思就和图示的一样,由于我们一次batch的任务可能会有很多的数据读写操作,因此一条一条的处理并向数据库提交的话效率不会很高,因此spring batch提供了chunk这个概念,我们可以设定一个chunk size,spring batch 将一条一条处理数据,但不提交到数据库,只有当处理的数据数量达到chunk size设定的值得时候,才一起去commit.

    java的实例定义代码如下:

    在上面这个step里面,chunk size被设为了10,当ItemReader读的数据数量达到10的时候,这一批次的数据就一起被传到itemWriter,同时transaction被提交。

    1、skip策略和失败处理

    一个batch的job的step,可能会处理非常大数量的数据,难免会遇到出错的情况,出错的情况虽出现的概率较小,但是我们不得不考虑这些情况,因为我们做数据迁移最重要的是要保证数据的最终一致性。spring batch当然也考虑到了这种情况,并且为我们提供了相关的技术支持,请看如下bean的配置:

    我们需要留意这三个方法,分别是skipLimit(),skip(),noSkip(),

    skipLimit方法的意思是我们可以设定一个我们允许的这个step可以跳过的异常数量,假如我们设定为10,则当这个step运行时,只要出现的异常数目不超过10,整个step都不会fail。注意,若不设定skipLimit,则其默认值是0.

    skip方法我们可以指定我们可以跳过的异常,因为有些异常的出现,我们是可以忽略的。

    noSkip方法的意思则是指出现这个异常我们不想跳过,也就是从skip的所以exception当中排除这个exception,从上面的例子来说,也就是跳过所有除FileNotFoundException的exception。那么对于这个step来说,FileNotFoundException就是一个fatal的exception,抛出这个exception的时候step就会直接fail


    批处理操作指南


    本部分是一些使用spring batch时的值得注意的点


    1、批处理原则

    在构建批处理解决方案时,应考虑以下关键原则和注意事项。

    • 批处理体系结构通常会影响体系结构

    • 尽可能简化并避免在单批应用程序中构建复杂的逻辑结构

    • 保持数据的处理和存储在物理上靠得很近(换句话说,将数据保存在处理过程中)。

    • 最大限度地减少系统资源的使用,尤其是I / O. 在internal memory中执行尽可能多的操作。

    • 查看应用程序I / O(分析SQL语句)以确保避免不必要的物理I / O. 特别是,需要寻找以下四个常见缺陷:

    1. 当数据可以被读取一次并缓存或保存在工作存储中时,读取每个事务的数据。

    2. 重新读取先前在同一事务中读取数据的事务的数据。

    3. 导致不必要的表或索引扫描。

    4. 未在SQL语句的WHERE子句中指定键值。

  • 在批处理运行中不要做两次一样的事情。例如,如果需要数据汇总以用于报告目的,则应该(如果可能)在最初处理数据时递增存储的总计,因此您的报告应用程序不必重新处理相同的数据。

  • 在批处理应用程序开始时分配足够的内存,以避免在此过程中进行耗时的重新分配。

  • 总是假设数据完整性最差。插入适当的检查和记录验证以维护数据完整性。

  • 尽可能实施校验和以进行内部验证。例如,对于一个文件里的数据应该有一个数据条数纪录,告诉文件中的记录总数以及关键字段的汇总。

  • 在具有真实数据量的类似生产环境中尽早计划和执行压力测试。

  • 在大批量系统中,数据备份可能具有挑战性,特别是如果系统以24-7在线的情况运行。数据库备份通常在在线设计中得到很好的处理,但文件备份应该被视为同样重要。如果系统依赖于文件,则文件备份过程不仅应该到位并记录在案,还应定期进行测试。

  • 2、如何默认不启动job

    在使用java config使用spring batch的job时,如果不做任何配置,项目在启动时就会默认去跑我们定义好的批处理job。那么如何让项目在启动时不自动去跑job呢?

    spring batch的job会在项目启动时自动run,如果我们不想让他在启动时run的话,可以在application.properties中添加如下属性:

    spring.batch.job.enabled=false


    3、在读数据时内存不够

    在使用spring batch做数据迁移时,发现在job启动后,执行到一定时间点时就卡在一个地方不动了,且log也不再打印,等待一段时间之后,得到如下错误:

    红字的信息为:Resource exhaustion event:the JVM was unable to allocate memory from the heap.

    翻译过来的意思就是项目发出了一个资源耗尽的事件,告诉我们java虚拟机无法再为堆分配内存。

    造成这个错误的原因是: 这个项目里的batch job的reader是一次性拿回了数据库里的所有数据,并没有进行分页,当这个数据量太大时,就会导致内存不够用。解决的办法有两个:

    • 调整reader读数据逻辑,按分页读取,但实现上会麻烦一些,且运行效率会下降
    • 增大service内存
    
    

  • 如何设计API返回码(错误码)?

    快乐分享,Java干货及时送达👇

    来源:ken.io/note/api-errorcode-or-resultcode-desgin
    • 一、前言
    • 二、HTTP 状态码参考
    • 三、参数约定
    • 四、个性化 Message
    • 五、返回信息的统一处理

    一、前言

    客户端请求 API,通常需要通过返回码来判断 API 返回的结果是否符合预期,以及该如何处理返回的内容等

    相信很多同学都吃过返回码定义混乱的亏,有的 API 用返回码是 int 类型,有的是 string 类型,有的用 0 表示成功,又有的用 1 表示成功,还有用”true” 表示成功,碰上这种事情,只能说:头疼

    API 返回码的设计还是要认真对待,毕竟好的返回码设计可以降低沟通成本以及程序的维护成本

    二、HTTP 状态码参考

    以 HTTP 状态码为例,为了更加清晰的表述和区分状态码的含义,HTTP 状态做了分段。

    图片

    对于后端开发来说,我们通常见到的都是:

    2XX 状态码,比如 200-> 请求成功,

    5XX 状态码,比如 502-> 服务器异常,通常就是服务没正常运行,或者代码执行出错

    通过状态码即可初步判断问题原因,HTTP 状态的设计思路值得借鉴。

    三、参数约定

    虽说是返回码设计,但是只有 code 是不行的,还要有对应的 message,让人可以看懂

    参考 HTTP 状态码的思路,我们对错误码进行分段

    通过这样的设计,不论是程序还是人都可以非常方便的区分 API 的返回结果,关键是统一!

    四、个性化 Message

    通常我们的 message 都是写给工程师看的,但是在不同的场景下,同样的错误,可能需要给用户看到不一样的错误提示。

    比方说 20000-29999 表示订单创建失败:

    • 20001,订单创建失败,存在进行中的订单
    • 20002,订单创建失败,上一个订单正在排队创建中

    这两种错误情况如果是给用户看,可能就只适合看到:很抱歉,您有一个正在进行中的订单,请到我的订单列表中处理。

    但是对于 API 来说,返回的信息又必须是准确的,但用户看到的就必须转译,这个转译的工作调用方可以做,但是通常 API 提供者来提供个性化的 Message 能力会更好

    我们可以把转译的消息配置到数据库,并缓存到 Redis 或者 API 本机

    图片

    然后在请求处理结束即将返回的时候,根据 application_id+code,去匹配替换 message

    图片

    这样我们就可以让手机 APP 的用户、微信小程序的用户、网页下单的企业用户看到不同的消息

    五、返回信息的统一处理

    有了统一的 code,我们就可以通过 Nginx 或者 APM 工具统计 API 请求 Code 数量及分布信息。

    我们可以根据单位时间内 99999 的数量来做 API 的异常告警

    我们可以根据 Code 的返回饼图,帮助我们发现系统、业务流程中的问题

    等等

    总之,好的返回码设计,可以帮助我们提高沟通效率,降低代码的维护成本。

    
    

  • 3 个腾讯开源的 GitHub 项目,足够惊艳!

    快乐分享,Java干货及时送达👇

    来源:网络

      01、系统清理工具

      腾讯开源了一个系统清理工具:腾讯柠檬清理,该软件可以系统性解决 macOS 设备空间问题。

      重点聚焦清理功能,对上百款软件提供定制化的清理方案,提供专业的清理建议,帮助用户轻松完成一键式的清理。

      主要功能包括:深度扫描清理、大文件清理、重复文件清理、相似照片清理、浏览器隐私清理、应用卸载、开启启动项管理、自定义状态栏展示信息。

      开源地址:

      https://github.com/Tencent/lemon-cleaner

      02、开源的 Markdown 编辑器

      Cherry Markdown Editor 是一款 Javascript Markdown 编辑器,具有开箱即用、轻量简洁、易于扩展等特点,它可以运行在浏览器或服务端 (NodeJs).

      当 Cherry Markdown 编辑器支持的语法不满足开发者需求时,可以快速的进行二次开发或功能扩展。

      同时,CherryMarkdown 编辑器应该由纯 JavaScript 实现,不应该依赖  Angular、Vue、React 等框架技术,框架只提供容器环境即可。

      开源地址:

      https://github.com/Tencent/cherry-markdown

      支持 Markdown 语法

      表格支持

      图标

      多光标批量编辑

      03、代码安全指南

      面向开发人员梳理的代码安全指南,旨在梳理 API 层面的风险点并提供详实可行的安全编码方案。该代码安全指南可用于开发人员日常参考或者修改漏洞时进行修复指引。

      开源地址:

      https://github.com/Tencent/secguide


    • 废物利用,拿自己的旧电脑搭建个服务器吧!

      最近总是想搭建自己的网站,奈何皮夹里空空如也,服务器也租不起,更别说域名了。于是我就寻思能否自己搭建个服务器,还不要钱呢?

      还真行!!!

      经过几天的冲浪,我发现有两个免费的建站工具:Apache和Nginx

      由于两个工具建站方法差不多,所以我就以Nginx为例

      1.安装Nginx

      首先前往Nginx官网(nginx.org)进行下载,也可以直接用我提供的链接下载1.23版本:http://nginx.org/download/nginx-1.23.1.zip

      安装完之后解压,然后你会看到如下目录:

      由于Nginx的功能很多,而我们今天只是搭建个服务器,所以只会用到其中的一部分。

      2.配置Nginx

      进入conf文件夹,打开nginx.conf文件进行编辑,里面的配置很多,我对其中一些重要的配置进行了说明(前面有“#”的表示并没有真正写入配置,若要加入,只需去掉“#”):

      3.启动Nginx服务

      配置完Nginx后,返回Nginx根目录,找到nginx.exe,双击运行它,你会看到有个小黑框一闪而过,这说明Nginx已经成功启动!你可以打开浏览器,输入:虚拟主机名称:监听的端口(刚刚的配置),回车,就会看到如下网页:

      恭喜你,已经成功搭建了Nginx服务器!

      4.为你的网站添加文件

      光开启了服务可还不够,如果别人看到你的网站只有干巴巴的一段文字,有什么用?接下来,进入刚刚配置的文件夹位置,在该文件夹下新建一个txt,打开后输入这段代码:

      html>
      html lang="en">
      head>
          meta charset="UTF-8">
          meta name="viewport" content="width=device-width, initial-scale=1.0">
          title>Documenttitle>
          style>
              * {
                  margin0;
                  padding0;
              }
              html {
                  height100%;
              }
              body {
                  height100%;
              }
              .container {
                  height100%;
                  background-imagelinear-gradient(to right, #fbc2eb, #a6c1ee);
              }
              .login-wrapper {
                  background-color#fff;
                  width358px;
                  height588px;
                  border-radius15px;
                  padding0 50px;
                  position: relative;
                  left50%;
                  top50%;
                  transformtranslate(-50%, -50%);
              }
              .header {
                  font-size38px;
                  font-weight: bold;
                  text-align: center;
                  line-height200px;
              }
              .input-item {
                  display: block;
                  width100%;
                  margin-bottom20px;
                  border0;
                  padding10px;
                  border-bottom1px solid rgb(128125125);
                  font-size15px;
                  outline: none;
              }
              .input-item:placeholder {
                  text-transform: uppercase;
              }
              .btn {
                  text-align: center;
                  padding10px;
                  width100%;
                  margin-top40px;
                  background-imagelinear-gradient(to right, #a6c1ee, #fbc2eb);
                  color#fff;
              }
              .msg {
                  text-align: center;
                  line-height88px;
              }
              a {
                  text-decoration-line: none;
                  color#abc1ee;
              }
          
      style>
      head>
      body>
          div class="container">
              div class="login-wrapper">
                  div class="header">Logindiv>
                  div class="form-wrapper">
                      input type="text" name="username" placeholder="username" class="input-item">
                      input type="password" name="password" placeholder="password" class="input-item">
                      div class="btn">Logindiv>
                  div>
                  div class="msg">
                      Don't have account?
                      a href="#">Sign upa>
                  div>
              div>
          div>
      body>
      html>

      再将文件名改为index.html,保存,最后再次打开浏览器,输入虚拟主机名称:监听的端口(刚刚的配置),回车,你就会看见如下页面:

      是不是非常好看?这个index.html其实是用HTML+CSS写出来的,感兴趣的同学可以去学习一下。

      除了html文件,你还可以在该文件夹里放任何文件,如:图片,视频,压缩包等等。

      5.内网穿透

      服务器搭建完了,网页也有了,但其实除了跟你在同一个局域网下的人,都无法访问你的网站。

      这里就要用到内网穿透了,所谓内网穿透,也即是局域网能够直接通过公网的ip去访问,极大的方便用户的日常远程的一些操作的使用。这里我建议大家使用飞鸽内网穿透,使用方法如下:

      5.1 注册

      进入飞鸽内网穿透官网,进行注册,这步就不多讲了。

      5.2 开通隧道

      注册好后,我们点击“开通隧道”选项,选择“免费节点”,有实力的也可以选贵的。

      然后填写信息,其中前置域名可以自定,本地ip端口一定要设置成:你的内网ip:刚配置的端口号。

      最后点击确认开通,就OK了,这样你就得到了免费域名+免费公网ip。

      5.3 启动服务

      点击此链接,根据电脑系统下载客户端。下载完后解压,一共有两个文件:傻瓜式运行点击我.vbs和npc.exe。

      点击傻瓜式运行点击我.vbs,打开后会看见一个弹窗,让你填写指令。我们切回飞鸽官网,点击“隧道管理”,如下图:根据电脑系统选择指令,点击复制,然后切回刚才的弹窗,将指令输入进去,点击确定。

      这样内网穿透就成功了!打开浏览器,输入刚才开通的隧道的访问地址(上图被抹掉的地方),回车,同样能开到之前编写的网页,就成功了。

      来源:blog.csdn.net/Lucas0623/article/

      details/126584307

    • Google Guava 工具包用起来太爽了!

      快乐分享,Java干货及时送达👇

      文章来源:https://www.jianshu.com/p/97778b21bd00

      前言


      目前Google Guava在实际应用中非常广泛,本篇博客将以博主对Guava使用的认识以及在项目中的经验来给大家分享!正如标题所言,学习使用Google Guava可以让你快乐编程,写出优雅的JAVA代码!


      以面向对象思想处理字符串:

      Joiner/Splitter/CharMatcher


      JDK提供的String还不够好么?

      也许还不够友好,至少让我们用起来还不够爽,还得操心!

      举个栗子,比如String提供的split方法,我们得关心空字符串吧,还得考虑返回的结果中存在null元素吧,只提供了前后trim的方法(如果我想对中间元素进行trim呢)。

      那么,看下面的代码示例,guava让你不必在操心这些:

      Joiner/Splitter

      Joiner是连接器,Splitter是分割器,通常我们会把它们定义为static final,利用on生成对象后在应用到String进行处理,这是可以复用的。要知道apache commons StringUtils提供的都是static method。更加重要的是,guava提供的Joiner/Splitter是经过充分测试,它的稳定性和效率要比apache高出不少,这个你可以自行测试下~

      发现没有我们想对String做什么操作,就是生成自己定制化的Joiner/Splitter,多么直白,简单,流畅的API!

      对于Joiner,常用的方法是  跳过NULL元素:skipNulls()  /  对于NULL元素使用其他替代:useForNull(String)

      对于Splitter,常用的方法是:trimResults()/omitEmptyStrings()。注意拆分的方式,有字符串,还有正则,还有固定长度分割(太贴心了!)

      其实除了Joiner/Splitter外,guava还提供了字符串匹配器:CharMatcher

      CharMatcher

      CharMatcher,将字符的匹配和处理解耦,并提供丰富的方法供你使用!

      对基本类型进行支持

      guava对JDK提供的原生类型操作进行了扩展,使得功能更加强大!

      Ints

      guava提供了Bytes/Shorts/Ints/Iongs/Floats/Doubles/Chars/Booleans这些基本数据类型的扩展支持,只有你想不到的,没有它没有的!


      对JDK集合的有效补充


      灰色地带:Multiset

      JDK的集合,提供了有序且可以重复的List,无序且不可以重复的Set。那这里其实对于集合涉及到了2个概念,一个order,一个dups。那么List vs Set,and then some ?

      Multiset

      Multiset是什么,我想上面的图,你应该了解它的概念了。Multiset就是无序的,但是可以重复的集合,它就是游离在List/Set之间的“灰色地带”!

      (至于有序的,不允许重复的集合嘛,guava还没有提供,当然在未来应该会提供UniqueList,我猜的,哈哈)

      来看一个Multiset的示例:

      Multiset Code

      Multiset自带一个有用的功能,就是可以跟踪每个对象的数量。


      Immutable vs unmodifiable


      来我们先看一个unmodifiable的例子:


      unmodifiable

      你看到JDK提供的unmodifiable的缺陷了吗?

      实际上,Collections.unmodifiableXxx所返回的集合和源集合是同一个对象,只不过可以对集合做出改变的API都被override,会抛出UnsupportedOperationException。

      也即是说我们改变源集合,导致不可变视图(unmodifiable View)也会发生变化,oh my god!

      当然,在不使用guava的情况下,我们是怎么避免上面的问题的呢?

      defensive copies

      上面揭示了一个概念:Defensive Copies,保护性拷贝。

      OK,unmodifiable看上去没有问题呢,但是guava依然觉得可以改进,于是提出了Immutable的概念,来看:

      Immutable

      就一个copyOf,你不会忘记,如此cheap~

      用Google官方的说法是:we’re using just one class,just say exactly what we mean,很了不起吗(不仅仅是个概念,Immutable在COPY阶段还考虑了线程的并发性等,很智能的!),O(∩_∩)O哈哈~

      guava提供了很多Immutable集合,比如ImmutableList/ImmutableSet/ImmutableSortedSet/ImmutableMap/……

      看一个ImmutableMap的例子:

      ImmutableMap


      可不可以一对多:Multimap

      JDK提供给我们的Map是一个键,一个值,一对一的,那么在实际开发中,显然存在一个KEY多个VALUE的情况(比如一个分类下的书本),我们往往这样表达:Map>,好像有点臃肿!臃肿也就算了,更加不爽的事,我们还得判断KEY是否存在来决定是否new 一个LIST出来,有点麻烦!更加麻烦的事情还在后头,比如遍历,比如删除,so hard……

      来看guava如何替你解决这个大麻烦的:

      Multimap

      友情提示下,guava所有的集合都有create方法,这样的好处在于简单,而且我们不必在重复泛型信息了。

      get()/keys()/keySet()/values()/entries()/asMap()都是非常有用的返回view collection的方法。

      Multimap的实现类有:ArrayListMultimap/HashMultimap/LinkedHashMultimap/TreeMultimap/ImmutableMultimap/……


      可不可以双向:BiMap

      JDK提供的MAP让我们可以find value by key,那么能不能通过find key by value呢,能不能KEY和VALUE都是唯一的呢。这是一个双向的概念,即forward+backward。

      在实际场景中有这样的需求吗?比如通过用户ID找到mail,也需要通过mail找回用户名。没有guava的时候,我们需要create forward map AND create backward map,and now just let guava do that for you.

      BiMap

      biMap / biMap.inverse() / biMap.inverse().inverse() 它们是什么关系呢?

      你可以稍微看一下BiMap的源码实现,实际上,当你创建BiMap的时候,在内部维护了2个map,一个forward map,一个backward map,并且设置了它们之间的关系。

      因此,biMap.inverse()  != biMap ;biMap.inverse().inverse() == biMap


      可不可以多个KEY:Table


      我们知道数据库除了主键外,还提供了复合索引,而且实际中这样的多级关系查找也是比较多的,当然我们可以利用嵌套的Map来实现:Map>。为了让我们的代码看起来不那么丑陋,guava为我们提供了Table。

      Table

      Table涉及到3个概念:rowKey,columnKey,value,并提供了多种视图以及操作方法让你更加轻松的处理多个KEY的场景。

      函数式编程:Functions

      Functions

      上面的代码是为了完成将List集合中的元素,先截取5个长度,然后转成大写。

      函数式编程的好处在于在集合遍历操作中提供自定义Function的操作,比如transform转换。我们再也不需要一遍遍的遍历集合,显著的简化了代码!

      对集合的transform操作可以通过Function完成

      断言:Predicate


      Predicate最常用的功能就是运用在集合的过滤当中!

      filter

      需要注意的是Lists并没有提供filter方法,不过你可以使用Collections2.filter完成!


      check null and other:

      Optional、Preconditions

      在guava中,对于null的处理手段是快速失败,你可以看看guava的源码,很多方法的第一行就是:Preconditions.checkNotNull(elements);

      要知道null是模糊的概念,是成功呢,还是失败呢,还是别的什么含义呢?

      Preconditions/Optional

      Cache is king

      对于大多数互联网项目而言,缓存的重要性,不言而喻!

      如果我们的应用系统,并不想使用一些第三方缓存组件(如redis),我们仅仅想在本地有一个功能足够强大的缓存,很可惜JDK提供的那些SET/MAP还不行!

      CacheLoader

      首先,这是一个本地缓存,guava提供的cache是一个简洁、高效,易于维护的。为什么这么说呢?因为并没有一个单独的线程用于刷新 OR 清理cache,对于cache的操作,都是通过访问/读写带来的,也就是说在读写中完成缓存的刷新操作!

      其次,我们看到了,我们非常通俗的告诉cache,我们的缓存策略是什么,SO EASY!在如此简单的背后,是guava帮助我们做了很多事情,比如线程安全。

      让异步回调更加简单

      JDK中提供了Future/FutureTask/Callable来对异步回调进行支持,但是还是看上去挺复杂的,能不能更加简单呢?比如注册一个监听回调。

      异步回调

      我们可以通过guava对JDK提供的线程池进行装饰,让其具有异步回调监听功能,然后在设置监听器即可!


      Summary

      到这里,这篇文章也只介绍了guava的冰山一角,其实还有很多内容:

      guava package

      比如反射、注解、网络、并发、IO等等

      好了,希望这篇文章让你快速进阶,快乐编程!

      
      

    • 面试官:一千万的数据,你是怎么查询的?

      快乐分享,Java干货及时送达👇

      来源:juejin.cn/post/6863668253898735629
      • 前言

      • 准备数据

        • 创建表

        • 创建数据脚本

      • 开始测试

        • 普通分页查询

      • 如何优化

        • 优化偏移量大问题

        • 优化数据量大问题

      • SELECT * 它不香吗?

      • 结束


      前言

      • 面试官:来说说,一千万的数据,你是怎么查询的?
      • B哥:直接分页查询,使用limit分页。
      • 面试官:有实操过吗?
      • B哥:肯定有呀

      此刻献上一首《凉凉》

      也许有些人没遇过上千万数据量的表,也不清楚查询上千万数据量的时候会发生什么。

      今天就来带大家实操一下,这次是基于MySQL 5.7.26做测试

      准备数据

      没有一千万的数据怎么办?

      创建呗

      代码创建一千万?那是不可能的,太慢了,可能真的要跑一天。可以采用数据库脚本执行速度快很多。

      创建表

      CREATE TABLE `user_operation_log`  (
        `id` int(11) NOT NULL AUTO_INCREMENT,
        `user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `ip` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `op_data` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr4` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr6` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr7` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr8` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr9` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr10` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr11` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        `attr12` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
        PRIMARY KEY (`id`) USING BTREE
      ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

      创建数据脚本

      采用批量插入,效率会快很多,而且每1000条数就commit,数据量太大,也会导致批量插入效率慢

      DELIMITER ;;
      CREATE PROCEDURE batch_insert_log()
      BEGIN
        DECLARE i INT DEFAULT 1;
        DECLARE userId INT DEFAULT 10000000;
       set @execSql = 'INSERT INTO `test`.`user_operation_log`(`user_id`, `ip`, `op_data`, `attr1`, `attr2`, `attr3`, `attr4`, `attr5`, `attr6`, `attr7`, `attr8`, `attr9`, `attr10`, `attr11`, `attr12`) VALUES';
       set @execData = '';
        WHILE i   set @attr = "'测试很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长的属性'";
        set @execData = concat(@execData, "(", userId + i, ", '10.0.69.175', '用户登录操作'"",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ")");
        if i % 1000 = 0
        then
           set @stmtSql = concat(@execSql, @execData,";");
          prepare stmt from @stmtSql;
          execute stmt;
          DEALLOCATE prepare stmt;
          commit;
          set @execData = "";
         else
           set @execData = concat(@execData, ",");
         end if;
        SET i=i+1;
        END WHILE;

      END;;
      DELIMITER ;

      开始测试

      哥的电脑配置比较低:win10 标压渣渣i5 读写约500MB的SSD

      由于配置低,本次测试只准备了3148000条数据,占用了磁盘5G(还没建索引的情况下),跑了38min,电脑配置好的同学,可以插入多点数据测试

      SELECT count(1) FROM `user_operation_log`

      返回结果:3148000

      三次查询时间分别为:

      • 14060 ms
      • 13755 ms
      • 13447 ms

      普通分页查询

      MySQL 支持 LIMIT 语句来选取指定的条数数据, Oracle 可以使用 ROWNUM 来选取。

      MySQL分页查询语法如下:

      SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset
      • 第一个参数指定第一个返回记录行的偏移量
      • 第二个参数指定返回记录行的最大数目

      下面我们开始测试查询结果:

      SELECT * FROM `user_operation_log` LIMIT 10000, 10

      查询3次时间分别为:

      • 59 ms
      • 49 ms
      • 50 ms

      这样看起来速度还行,不过是本地数据库,速度自然快点。

      换个角度来测试

      相同偏移量,不同数据量

      SELECT * FROM `user_operation_log` LIMIT 10000, 10
      SELECT * FROM `user_operation_log` LIMIT 10000, 100
      SELECT * FROM `user_operation_log` LIMIT 10000, 1000
      SELECT * FROM `user_operation_log` LIMIT 10000, 10000
      SELECT * FROM `user_operation_log` LIMIT 10000, 100000
      SELECT * FROM `user_operation_log` LIMIT 10000, 1000000

      查询时间如下:

      图片

      从上面结果可以得出结束:数据量越大,花费时间越长

      相同数据量,不同偏移量

      SELECT * FROM `user_operation_log` LIMIT 100, 100
      SELECT * FROM `user_operation_log` LIMIT 1000, 100
      SELECT * FROM `user_operation_log` LIMIT 10000, 100
      SELECT * FROM `user_operation_log` LIMIT 100000, 100
      SELECT * FROM `user_operation_log` LIMIT 1000000, 100

      图片

      从上面结果可以得出结束:偏移量越大,花费时间越长

      SELECT * FROM `user_operation_log` LIMIT 100, 100
      SELECT id, attr FROM `user_operation_log` LIMIT 100, 100

      如何优化

      既然我们经过上面一番的折腾,也得出了结论,针对上面两个问题:偏移大、数据量大,我们分别着手优化

      优化偏移量大问题

      采用子查询方式

      我们可以先定位偏移位置的 id,然后再查询数据

      SELECT * FROM `user_operation_log` LIMIT 1000000, 10SELECT id FROM `user_operation_log` LIMIT 1000000, 1SELECT * FROM `user_operation_log` WHERE id >= (SELECT id FROM `user_operation_log` LIMIT 1000000, 1) LIMIT 10

      查询结果如下:

      从上面结果得出结论:

      • 第一条花费的时间最大,第三条比第一条稍微好点
      • 子查询使用索引速度更快

      缺点:只适用于id递增的情况

      id非递增的情况可以使用以下写法,但这种缺点是分页查询只能放在子查询里面

      注意:某些 mysql 版本不支持在 in 子句中使用 limit,所以采用了多个嵌套select

      SELECT * FROM `user_operation_log` WHERE id IN (SELECT t.id FROM (SELECT id FROM `user_operation_log` LIMIT 1000000, 10) AS t)

      采用 id 限定方式

      这种方法要求更高些,id必须是连续递增,而且还得计算id的范围,然后使用 between,sql如下

      SELECT * FROM `user_operation_log` WHERE id between 1000000 AND 1000100 LIMIT 100

      SELECT * FROM `user_operation_log` WHERE id >= 1000000 LIMIT 100

      查询结果如下:

      图片

      从结果可以看出这种方式非常快

      注意:这里的 LIMIT 是限制了条数,没有采用偏移量

      优化数据量大问题

      返回结果的数据量也会直接影响速度

      SELECT * FROM `user_operation_log` LIMIT 1, 1000000

      SELECT id FROM `user_operation_log` LIMIT 1, 1000000

      SELECT id, user_id, ip, op_data, attr1, attr2, attr3, attr4, attr5, attr6, attr7, attr8, attr9, attr10, attr11, attr12 FROM `user_operation_log` LIMIT 1, 1000000

      查询结果如下:

      图片

      从结果可以看出减少不需要的列,查询效率也可以得到明显提升

      第一条和第三条查询速度差不多,这时候你肯定会吐槽,那我还写那么多字段干啥呢,直接 * 不就完事了

      注意本人的 MySQL 服务器和客户端是在_同一台机器_上,所以查询数据相差不多,有条件的同学可以测测客户端与MySQL分开

      SELECT * 它不香吗?

      在这里顺便补充一下为什么要禁止 SELECT *。难道简单无脑,它不香吗?

      主要两点:

      • 用 “SELECT * ” 数据库需要解析更多的对象、字段、权限、属性等相关内容,在 SQL 语句复杂,硬解析较多的情况下,会对数据库造成沉重的负担。
      • 增大网络开销,* 有时会误带上如log、IconMD5之类的无用且大文本字段,数据传输size会几何增涨。特别是MySQL和应用程序不在同一台机器,这种开销非常明显。

      结束

      最后还是希望大家自己去实操一下,肯定还可以收获更多,欢迎留言!!

      创建脚本我给你正好了,你还在等什么!!!

      
      

    • 偷学一波 Vue 3 !

      背景

      大家好,我是小哈~

      最近私底下准备整个前后端分离的博客项目,前端这块在技术选型上选择了 Vue 3,但是对于一个搞后端的,这块是盲点,虽然以前在中台的时候,前端组因为人手有限需要后端成员自行学会联调接口,也给咱培训了一下基础的用法(内心是拒绝的),但是也好久没再碰了,现在只有大致的印象。

      而且 Vue 3 相对于 Vue 2 又更新了一些新特性, 于是学习了一波,在这里给大家分享出来,算是入门。

      PS: 教程第一时间会发布在个站犬小哈教程上:www.quanxiaoha.com

      目录

      • 什么是 Vue ?

        • 渐进式框架

        • 组件化

      • Vue 3 环境安装

        • 第一步:安装 Node.js 环境

        • 第二步:验证是否真的安装成功了

      • 创建第一个 Vue 3 项目

        • 项目目录说明

      • 启动项目

      • 打包项目

      • 安装 VSCode

        • VSCode 简介

        • 下载安装包

        • 开始安装

        • 使用界面

      • VSCode 设置中文

        • 开始设置

        • 汉化后的界面

      • 开发 Vue 3 必备的 VSCode 插件

        • 一、Vue Language Features (Volar) 插件

        • 二、Vue 3 Snippets 插件

        • 如何安装插件?

      • 使用 VSCode 开发第一个 Vue 应用

        • 打开项目

        • 核心文件说明

      什么是 Vue ?

      Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 渐进式框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、响应式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以轻松搞定。

      下面是一段示例代码,其功能用于实现一个简单的计数器:

      import { createApp } from 'vue'

      createApp({
        data() {
          return {
            count: 0
          }
        }
      }).mount('#app')
      "app">
        
        

      上面的示例展示了 Vue 两个核心的功能点:

      • 声明式:Vue 基于标准的 HTML 语法上做了一层拓展,我们可以通过声明式的描述 HTML 与 JavaScript 状态之间的关系,如示例中的插值语句 {{ count }}、点击事件 @click="count++"
      • 响应式渲染 :Vue 会自动跟踪 JavaScript 状态,并实时更新 Dom 元素。无需再像 JQuery 那样手动更新 Dom 元素。

      渐进式框架

      Vue 是一个功能强大框架,也是一个生态。你可以用不同的方式来使用它:

      • 无需构建步骤,渐进式增强静态的 HTML
      • 在任何页面中作为 Web Components 嵌入
      • 单页应用 (SPA)
      • 全栈 / 服务端渲染 (SSR)
      • Jamstack / 静态站点生成 (SSG)
      • 开发桌面端、移动端、WebGL,甚至是命令行终端中的界面

      怎么理解渐进式这个词?

      你可以这样理解它:Vue 非常灵活,可以渐近式的适配不同的开发场景。举个栗子,比如老项目使用的 JQuery,而我又想使用 Vue, 新建页面时,仅需引入 Vue 的库,就可以通过它来开发了,无需构建步骤。由此可见,Vue 在设计上非常注重灵活性,我们 “可以被逐步集成” 它。

      组件化

      当我们通过构建工具来创建项目时,会看到工程目录中有以 .vue 为后缀的类似 HTML 的文件,它们就是 Vue 组件,文件内部会将一个组件的逻辑(JavaScript), 模板(HTML) 和样式 (CSS)封装在一起。

      Vue 3 环境安装

      第一步:安装 Node.js 环境

      访问 Node.js 官网:https://nodejs.org/en,点击左侧的下载按钮,下载 Node.js LTS 版本的安装包:

      注意:学习 Vue 3, 你需要安装 Node.js 16.0 版本或者更高, LTS 表示该安装包是一个被长期支持的版本,可以理解成是一个稳定版本。

      下载 Node.js 安装包

      下载完成后,双击开始安装:

      Node.js 安装文件

      无脑点击下一步【Next】按钮即可,其中,需要勾选接受协议,以及自选选择安装路径,小哈这里直接使用的默认安装路径 C盘:

      安装 Node.js

      继续点击【Next】按钮, 然后进入安装:

      安装 Node.js

      等待其安装成功:

      Node.js 正在安装中

      然后点击【Finish】按钮,到这里 Node.js 就安装成功了:

      Node.js 安装完成

      第二步:验证是否真的安装成功了

      按住快捷键 win + R 输入 cmd 打开命令行,或者使用 PowerShell 等其他命令行工具,执行如下命令:

      node -v
      npm -v

      如果能够正确输出版本号,则表示 Vue 的环境搭建成功:

      确认 Node.js 环境是否安装成功

      创建第一个 Vue 3 项目

      D 盘下创建一个 vue-projects 文件夹,用于统一存放 Vue 项目,然后打开命令行,执行如下命令,进入到该文件夹中:

      cd D:vue-projects

      然后,执行初始化 Vue 项目命令:

      npm init vue@latest

      TIP: 该命令会安装并执行 create-vue, 它是 Vue 官方的项目脚手架工具。

      创建第一个 Vue 项目

      执行过程中,会提示你命名新项目,以及是否需要开启一些诸如 TypeScript 和测试支持之类的可选功能,这里如果不确定,统一敲击回车键选择 No 即可。当你看到命令行中提示 Done , 表示你已经创建好了第一个 Vue 应用。

      项目目录说明

      创建好了第一个 Vue 应用后,进入该项目文件夹,看下目录结构:

      Vue 3 应用目录结构

      解释一下目录结构以及相关文件的作用:

      • node_modules : 项目依赖包文件夹,比如通过 npm install 包名 安装的包都会放在这个目录下;
      • public : 公共资源目录,用于存放公共资源,如 favicon.ico 图标等;
      • index.html : 首页;
      • package.json : 项目描述以及依赖;
      • package-lock.json : 版本管理使用的文件;
      • README.md : 用于项目描述的 markdown 文档;
      • src : 核心文件目录,源码都放在这里面;

      进入 src 文件夹,目录如下:

      src 目录
      • assets : 静态资源目录,用于存放样式、图片、字体等;
      • components: 组件文件夹,通用的组件存放目录;
      • App.vue: 主组件,也是页面的入口文件,所有页面都是在 App.vue 下进行路由切换的;
      • main.js : 入口 Javascript 文件;

      启动项目

      进入到想要启动的项目文件夹中,执行如下命令,为项目创建依赖并执行:

      # 进入项目文件夹
      cd 
      # 安装项目所需依赖
      npm install
      # 启动项目
      npm run dev

      启动成功后,会提示项目的访问地址,如 http://localhost:5173/:

      启动 Vue 项目

      在浏览器地址栏中访问该地址,即可访问该 Vue 项目啦,整个过程还是非常简单的。

      Vue 启动页面展示

      打包项目

      首先,通过命令行进入到项目所在目录中,当需要将 Vue 项目打包发布到生产环境时,执行如下命令:

      npm run build
      打包 Vue 项目

      执行成功后,会在项目文件夹中看到多了一个 dist 文件夹:

      该文件下放置的就是编译后的静态文件,如 htmlcssjs 等相关文件:

      Vue 项目编译后的文件

      至此,该 Vue 项目就打包好了。

      安装 VSCode

      为了更高效率的开发 Vue 3,我们需要有个趁手的兵器,也就是开发工具。比较常见的如 VSCode 、Webstorm 等,但是官方推荐使用 VSCode, 那我们就通过 VSCode 来开发 Vue 3。

      VSCode 简介

      VSCode 全称 Visual Studio Code,是微软出的一款轻量级代码编辑器,它具有如下特点:

      • 开源且免费;
      • 代码智能提示、自动补全功能;
      • 可自定义配置;
      • 支持丰富的文件格式;
      • 代码调试功能强大;
      • 各种方便的快捷键;
      • 强大的插件拓展功能;

      下载安装包

      前往 VSCode 官网:https://code.visualstudio.com/ 下载对应系统的安装包,小哈这里用 Windows 系统来演示:

      官网下载 VSCode 安装包

      开始安装

      下载成功后,双击安装包开始安装 VSCode:

      VSCode 安装包

      勾选【我同意此协议】,点击下一步按钮:

      同意安装协议

      自定义安装路径,小哈这里安装在了 D 盘,可自行选择安装位置,继续点击下一步按钮:

      自定义 VSCode 安装路径

      继续点击下一步按钮:

      勾选【创建桌面快捷方式】,点击下一步:

      创建 VSCode 桌面快捷启动方式

      点击【安装】:

      开始安装 VSCode

      等待一分钟左右,即可安装成功,然后点击【完成】按钮:

      VSCode 安装完成

      使用界面

      启动成功后,即可看到如下界面,至此,VSCode 就安装成功啦~

      VSCode 界面

      VSCode 设置中文

      TIP: 汉化是可选项,针对初学者来说,全英文化的 VSCode 可能不太友好,所以,根据自己的需求来确定是否需要汉化,小哈个人推荐不要汉化,用着用着就习惯了。

      开始设置

      在 VSCode 的左侧栏,可以看到插件市场选项,如下图所示:

      VSCode 插件市场

      打开插件市场,搜索关键词【中文】,即可看到中文汉化插件,点击【Install】安装:

      VSCode 安装中文插件

      安装成功后,右下角会提示是否需要立刻重启 VSCode 来使汉化生效,点击重启:

      重启 VSCode

      汉化后的界面

      重启 VSCode 后,你就可以看到所有菜单均已被设置成中文了:

      中文汉化后的 VSCode

      开发 Vue 3 必备的 VSCode 插件

      本小节中,我们将在 VSCode 中安装上开发 Vue 3 必备的 2 个插件。

      一、Vue Language Features (Volar) 插件

      简介:这是一款专用于构建 Vue 的拓展,想要在 VSCode 上开发 Vue 3 应用,这款插件必不可少。

      Volar 插件

      二、Vue 3 Snippets 插件

      简介:Vue 3 代码自动提示和代码补全插件,提升编码效率。

      Vue 3 Snippets 插件

      如何安装插件?

      前面的VSCode 安装中文汉化插件一节中,已经详细演示了如何在 VSCode 中安装想要的插件,不清楚的小伙伴可以跳转前面小节查阅。这里就不重复讲了。

      使用 VSCode 开发第一个 Vue 应用

      前面小节中已经通过命令行创建了第一个 Vue 应用,本小节中,我们将通过 VSCode 来打开它,并通过 Vue 的双向绑定功能,实现一个简单的计数器功能。

      打开项目

      点击 VSCode 左上角菜单:文件 -> 打开文件夹,导入之前创建好的 vue-test 项目:

      TIP : 或者你也可以将项目文件夹直接拖入 VSCode 来打开项目。

      打开 Vue 项目

      导入成功后,视图如下:

      核心文件说明

      在前面创建项目小节中,我们已经了解了各个文件夹,以及文件的大致用途。这里小哈针对最核心的 3 个文件再详细说明一下,分别是:

      • index.html :首页;
      • main.js  :主 js 文件;
      • App.vue : 主组件;

      这 3 者之间的关系如下:

      依赖关系

      当打开一个 Vue 3 应用,首先先看 index.html 文件,它是首页,代码如下,这里小哈已经添加好注释说明:


      "en">
        
          "UTF-8">
          "icon" href="/favicon.ico">
          "viewport" content="width=device-width, initial-scale=1.0">
          Vite App
        
        
          
          
      "app">

          
          
        


      再来看 main.js 文件:

      import { createApp } from 'vue' // 引入 createApp 方法
      import App from './App.vue'     // 引入 App.vue 组件

      import './assets/main.css'      // 引入 main.css 样式文件

      // 创建应用,并将 App 根组件挂载到 
      "#app">
       中
      createApp(App).mount('#app')

      再看 app.vue 组件代码:






      作为初学者,为了更方便的学习,我们先将多余的代码删除掉,只保留结构,如下图所示:

      结构分为 3 个部分:

      • script : 节点中间用于放置 javascript 代码;

      • template : 节点中间用于放置 html 代码;

      • style : 节点中间用于放置 css 样式代码;






      然后,我们在 标签下添加一个

      标题:






      保存代码并刷新页面,效果如下:

      再次修改代码,添加一个用于计数的






      保存代码并刷新页面,点击按钮,可以看到当点击按钮时,会对 count 进行 +1 操作,由于该变量是个响应式的,变量数值变化后也会同步渲染到按钮上,非常简单就实现了一个双向绑定功能。

      结语

      本文中,小哈带着大家了解了 Vue, 以及上手安装了 Vue 3 的环境,最后通过官方推荐的 VSCode 开发工具开了第一个计数小项目,希望对学习 Vue 3 有兴趣的小伙伴有所帮助,后期还会持续分享 Vue 3 相关的文章,欢迎关注。

      
      

    • SpringBoot + K8S 中的滚动发布、优雅停机、弹性伸缩、应用监控、配置分离

      快乐分享,Java干货及时送达👇

      来源:blog.csdn.net/qq_14999375/article/details/123309636

      • 前言

      • 配置

        • 健康检查

        • 滚动更新

        • 弹性伸缩

        • Prometheus集成

        • 配置分离

      • 汇总配置

        • 业务层面

        • 运维层面


      前言

      K8s + SpringBoot实现零宕机发布:健康检查+滚动更新+优雅停机+弹性伸缩+Prometheus监控+配置分离(镜像复用)

      配置

      健康检查

      • 健康检查类型:就绪探针(readiness)+ 存活探针(liveness)
      • 探针类型:exec(进入容器执行脚本)、tcpSocket(探测端口)、httpGet(调用接口)
      业务层面

      项目依赖 pom.xml


          org.springframework.boot
          spring-boot-starter-actuator

      定义访问端口、路径及权限 application.yaml

      management:
        server:
          port: 50000                         # 启用独立运维端口
        endpoint:                             # 开启health端点
          health:
            probes:
              enabled: true
        endpoints:
          web:
            exposure:
              base-path: /actuator            # 指定上下文路径,启用相应端点
              include: health

      将暴露/actuator/health/readiness/actuator/health/liveness两个接口,访问方式如下:

      http://127.0.0.1:50000/actuator/health/readiness
      http://127.0.0.1:50000/actuator/health/liveness
      运维层面

      k8s部署模版deployment.yaml

      apiVersion: apps/v1
      kind: Deployment
      spec:
        template:
          spec:
            containers:
            - name: {APP_NAME}
              image: {IMAGE_URL}
              imagePullPolicy: Always
              ports:
              - containerPort: {APP_PORT}
              - name: management-port
                containerPort: 50000         # 应用管理端口
              readinessProbe:                # 就绪探针
                httpGet:
                  path: /actuator/health/readiness
                  port: management-port
                initialDelaySeconds: 30      # 延迟加载时间
                periodSeconds: 10            # 重试时间间隔
                timeoutSeconds: 1            # 超时时间设置
                successThreshold: 1          # 健康阈值
                failureThreshold: 6          # 不健康阈值
              livenessProbe:                 # 存活探针
                httpGet:
                  path: /actuator/health/liveness
                  port: management-port
                initialDelaySeconds: 30      # 延迟加载时间
                periodSeconds: 10            # 重试时间间隔
                timeoutSeconds: 1            # 超时时间设置
                successThreshold: 1          # 健康阈值
                failureThreshold: 6          # 不健康阈值

      滚动更新

      k8s资源调度之滚动更新策略,若要实现零宕机发布,需支持健康检查

      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: {APP_NAME}
        labels:
          app: {APP_NAME}
      spec:
        selector:
          matchLabels:
            app: {APP_NAME}
        replicas: {REPLICAS}    # Pod副本数
        strategy:
          type: RollingUpdate    # 滚动更新策略
          rollingUpdate:
            maxSurge: 1                   # 升级过程中最多可以比原先设置的副本数多出的数量
            maxUnavailable: 1             # 升级过程中最多有多少个POD处于无法提供服务的状态
      优雅停机

      在K8s中,当我们实现滚动升级之前,务必要实现应用级别的优雅停机。否则滚动升级时,还是会影响到业务。使应用关闭线程、释放连接资源后再停止服务

      业务层面

      项目依赖 pom.xml


          org.springframework.boot
          spring-boot-starter-actuator

      定义访问端口、路径及权限 application.yaml

      spring:
        application:
          name: 
        profiles:
          active: @profileActive@
        lifecycle:
          timeout-per-shutdown-phase: 30s     # 停机过程超时时长设置30s,超过30s,直接停机

      server:
        port: 8080
        shutdown: graceful                    # 默认为IMMEDIATE,表示立即关机;GRACEFUL表示优雅关机

      management:
        server:
          port: 50000                         # 启用独立运维端口
        endpoint:                             # 开启shutdown和health端点
          shutdown:
            enabled: true
          health:
            probes:
              enabled: true
        endpoints:
          web:
            exposure:
              base-path: /actuator            # 指定上下文路径,启用相应端点
              include: health,shutdown

      将暴露/actuator/shutdown接口,调用方式如下:

      curl -X POST 127.0.0.1:50000/actuator/shutdown
      运维层面

      确保dockerfile模版集成curl工具,否则无法使用curl命令

      FROM openjdk:8-jdk-alpine
      #构建参数
      ARG JAR_FILE
      ARG WORK_PATH="/app"
      ARG EXPOSE_PORT=8080

      #环境变量
      ENV JAVA_OPTS=""
          JAR_FILE=${JAR_FILE}

      #设置时区
      RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
      RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories  
          && apk add --no-cache curl
      #将maven目录的jar包拷贝到docker中,并命名为for_docker.jar
      COPY target/$JAR_FILE $WORK_PATH/


      #设置工作目录
      WORKDIR $WORK_PATH


      # 指定于外界交互的端口
      EXPOSE $EXPOSE_PORT
      # 配置容器,使其可执行化
      ENTRYPOINT exec java $JAVA_OPTS -jar $JAR_FILE

      k8s部署模版deployment.yaml

      注:经验证,java项目可省略结束回调钩子的配置

      此外,若需使用回调钩子,需保证镜像中包含curl工具,且需注意应用管理端口(50000)不能暴露到公网

      apiVersion: apps/v1
      kind: Deployment
      spec:
        template:
          spec:
            containers:
            - name: {APP_NAME}
              image: {IMAGE_URL}
              imagePullPolicy: Always
              ports:
              - containerPort: {APP_PORT}
              - containerPort: 50000
              lifecycle:
                preStop:       # 结束回调钩子
                  exec:
                    command: ["curl""-XPOST""127.0.0.1:50000/actuator/shutdown"]

      弹性伸缩

      为pod设置资源限制后,创建HPA

      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: {APP_NAME}
        labels:
          app: {APP_NAME}
      spec:
        template:
          spec:
            containers:
            - name: {APP_NAME}
              image: {IMAGE_URL}
              imagePullPolicy: Always
              resources:                     # 容器资源管理
                limits:                      # 资源限制(监控使用情况)
                  cpu: 0.5
                  memory: 1Gi
                requests:                    # 最小可用资源(灵活调度)
                  cpu: 0.15
                  memory: 300Mi
      ---
      kind: HorizontalPodAutoscaler            # 弹性伸缩控制器
      apiVersion: autoscaling/v2beta2
      metadata:
        name: {APP_NAME}
      spec:
        scaleTargetRef:
          apiVersion: apps/v1
          kind: Deployment
          name: {APP_NAME}
        minReplicas: {REPLICAS}                # 缩放范围
        maxReplicas: 6
        metrics:
          - type: Resource
            resource:
              name: cpu                        # 指定资源指标
              target:
                type: Utilization
                averageUtilization: 50

      Prometheus集成

      业务层面

      项目依赖 pom.xml



          org.springframework.boot
          spring-boot-starter-actuator


          io.micrometer
          micrometer-registry-prometheus

      定义访问端口、路径及权限 application.yaml

      management:
        server:
          port: 50000                         # 启用独立运维端口
        metrics:
          tags:
            application: ${spring.application.name}
        endpoints:
          web:
            exposure:
              base-path: /actuator            # 指定上下文路径,启用相应端点
              include: metrics,prometheus

      将暴露/actuator/metric/actuator/prometheus接口,访问方式如下:

      http://127.0.0.1:50000/actuator/metric
      http://127.0.0.1:50000/actuator/prometheus
      运维层面

      deployment.yaml

      apiVersion: apps/v1
      kind: Deployment
      spec:
        template:
          metadata:
            annotations:
              prometheus:io/port: "50000"
              prometheus.io/path: /actuator/prometheus  # 在流水线中赋值
              prometheus.io/scrape: "true"              # 基于pod的服务发现

      配置分离

      方案:通过configmap挂载外部配置文件,并指定激活环境运行

      作用:配置分离,避免敏感信息泄露;镜像复用,提高交付效率

      通过文件生成configmap

      # 通过dry-run的方式生成yaml文件
      kubectl create cm -n   --from-file=application-test.yaml --dry-run=1 -oyaml > configmap.yaml

      # 更新
      kubectl apply -f configmap.yaml

      挂载configmap并指定激活环境

      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: {APP_NAME}
        labels:
          app: {APP_NAME}
      spec:
        template:
          spec:
            containers:
            - name: {APP_NAME}
              image: {IMAGE_URL}
              imagePullPolicy: Always
              env:
                - name: SPRING_PROFILES_ACTIVE   # 指定激活环境
                  value: test
              volumeMounts:                      # 挂载configmap
              - name: conf
                mountPath: "/app/config"         # 与Dockerfile中工作目录一致
                readOnly: true
            volumes:
            - name: conf
              configMap:
                name: {APP_NAME}

      汇总配置

      业务层面

      项目依赖 pom.xml



          org.springframework.boot
          spring-boot-starter-actuator


          io.micrometer
          micrometer-registry-prometheus

      定义访问端口、路径及权限 application.yaml

      spring:
        application:
          name: project-sample
        profiles:
          active: @profileActive@
        lifecycle:
          timeout-per-shutdown-phase: 30s     # 停机过程超时时长设置30s,超过30s,直接停机

      server:
        port: 8080
        shutdown: graceful                    # 默认为IMMEDIATE,表示立即关机;GRACEFUL表示优雅关机

      management:
        server:
          port: 50000                         # 启用独立运维端口
        metrics:
          tags:
            application: ${spring.application.name}
        endpoint:                             # 开启shutdown和health端点
          shutdown:
            enabled: true
          health:
            probes:
              enabled: true
        endpoints:
          web:
            exposure:
              base-path: /actuator            # 指定上下文路径,启用相应端点
              include: health,shutdown,metrics,prometheus

      运维层面

      确保dockerfile模版集成curl工具,否则无法使用curl命令

      FROM openjdk:8-jdk-alpine
      #构建参数
      ARG JAR_FILE
      ARG WORK_PATH="/app"
      ARG EXPOSE_PORT=8080

      #环境变量
      ENV JAVA_OPTS=""
          JAR_FILE=${JAR_FILE}

      #设置时区
      RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
      RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories  
          && apk add --no-cache curl
      #将maven目录的jar包拷贝到docker中,并命名为for_docker.jar
      COPY target/$JAR_FILE $WORK_PATH/


      #设置工作目录
      WORKDIR $WORK_PATH


      # 指定于外界交互的端口
      EXPOSE $EXPOSE_PORT
      # 配置容器,使其可执行化
      ENTRYPOINT exec java $JAVA_OPTS -jar $JAR_FILE

      k8s部署模版deployment.yaml

      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: {APP_NAME}
        labels:
          app: {APP_NAME}
      spec:
        selector:
          matchLabels:
            app: {APP_NAME}
        replicas: {REPLICAS}                            # Pod副本数
        strategy:
          type: RollingUpdate                           # 滚动更新策略
          rollingUpdate:
            maxSurge: 1
            maxUnavailable: 0
        template:
          metadata:
            name: {APP_NAME}
            labels:
              app: {APP_NAME}
            annotations:
              timestamp: {TIMESTAMP}
              prometheus.io/port: "50000"               # 不能动态赋值
              prometheus.io/path: /actuator/prometheus
              prometheus.io/scrape: "true"              # 基于pod的服务发现
          spec:
            affinity:                                   # 设置调度策略,采取多主机/多可用区部署
              podAntiAffinity:
                preferredDuringSchedulingIgnoredDuringExecution:
                - weight: 100
                  podAffinityTerm:
                    labelSelector:
                      matchExpressions:
                      - key: app
                        operator: In
                        values:
                        - {APP_NAME}
                    topologyKey: "kubernetes.io/hostname" # 多可用区为"topology.kubernetes.io/zone"
            terminationGracePeriodSeconds: 30             # 优雅终止宽限期
            containers:
            - name: {APP_NAME}
              image: {IMAGE_URL}
              imagePullPolicy: Always
              ports:
              - containerPort: {APP_PORT}
              - name: management-port
                containerPort: 50000         # 应用管理端口
              readinessProbe:                # 就绪探针
                httpGet:
                  path: /actuator/health/readiness
                  port: management-port
                initialDelaySeconds: 30      # 延迟加载时间
                periodSeconds: 10            # 重试时间间隔
                timeoutSeconds: 1            # 超时时间设置
                successThreshold: 1          # 健康阈值
                failureThreshold: 9          # 不健康阈值
              livenessProbe:                 # 存活探针
                httpGet:
                  path: /actuator/health/liveness
                  port: management-port
                initialDelaySeconds: 30      # 延迟加载时间
                periodSeconds: 10            # 重试时间间隔
                timeoutSeconds: 1            # 超时时间设置
                successThreshold: 1          # 健康阈值
                failureThreshold: 6          # 不健康阈值
              resources:                     # 容器资源管理
                limits:                      # 资源限制(监控使用情况)
                  cpu: 0.5
                  memory: 1Gi
                requests:                    # 最小可用资源(灵活调度)
                  cpu: 0.1
                  memory: 200Mi
              env:
                - name: TZ
                  value: Asia/Shanghai
      ---
      kind: HorizontalPodAutoscaler            # 弹性伸缩控制器
      apiVersion: autoscaling/v2beta2
      metadata:
        name: {APP_NAME}
      spec:
        scaleTargetRef:
          apiVersion: apps/v1
          kind: Deployment
          name: {APP_NAME}
        minReplicas: {REPLICAS}                # 缩放范围
        maxReplicas: 6
        metrics:
          - type: Resource
            resource:
              name: cpu                        # 指定资源指标
              target:
                type: Utilization
                averageUtilization: 50

    • 这套开源系统太牛了!仅需一分钟,安装部署一套自己的 SAAS 云建站平台!

      项目介绍

      最近在逛网站的时候发现一个不错的开源项目,这个项目目前收获了 4.3K Star,觉得不错,值得拿出来和大家分享下。

      本项目系统是🔥一个可通过后台任意开通多个网站,每个网站使用自己的账号进行独立管理。让每个互联网公司都可私有化部署自己的SAAS云建站平台(延续了织梦、帝国CMS的模版方式,一台1核2G服务器可建立几万个独立网站。历经11年,不断完善,拒绝半成品!)。

      简介

      网站使用方面,延续了帝国CMS、织梦CMS的建站方式,有模版页面、模版变量、栏目绑定模版、内容管理等,用过帝国、织梦的,可快速使用!

      整体结构简介, SAAS云建站系统,可通过后台任意开通多个网站,每个网站使用自己的账号进行独立管理。让每个互联网公司都可私有化部署自己的SAAS云建站平台。

      建站服务人员,招聘一个计算机专业的大学生,懂点html、会点PS作图,就完全足够,刚毕业大学生具有认真、学习能力强、工资成本相对更省等优点,必须首选。至于后台Java开发人员、服务器运维,统统干掉不要,用本系统做网站,已经不需要服务器运维及Java开发。

      功能

      在线开通网站,无需任何操作服务器操作

      • 可通过后台(系统中的代理后台)在线开通网站

      • 用户可通过手机号+验证码方式自助开通网站(须配置短信通道购买短信验证码条数)

      域名及绑定

      • 开通的网站,系统自动分配一个二级域名,以供测试。(本系统安装时输入的域名,自动分配的二级域名就是从这个上自动分配出来的)

      • 网站可以绑定自己的顶级域名,在网站管理后台-域名设置 中,按照提示步骤进行设置、解析,即可完成绑定。

      • 如果网站想绑定多个顶级域名,可以在功能插件-多域名绑定中绑定多个。不过不建议一个网站绑定多个,多个对SEO优化不好

      模板

      • 模板采用 HTML 方式制作模板,可通过网站后台任意编写html(及js、css等)代码

      • 模板体系还包含模板变量(多个模板页中有公共的代码块,可以作为模板变量)、全局变量等,方便模板页中动态引用

      • 模板编辑时内置代码编辑器,更方便编辑书写代码

      • 内置半可视化的界面编辑(待升级完善,有垃圾代码产生,推荐用纯代码方式编辑)

      • 云端模板库百多套模板开放免费使用,是默认自带的,安装本系统后,创建一个网站,登录进入网站管理后台时,就可以看到选择模板这里

      网站访问及生成网站

      • 开源版本在网站访问时,会直接将服务器磁盘上的 html 文件拿来显示

      • 企业版在网站访问时,因为企业版采用云存储,html文件不在服务器,存在于云存储(分布式存储)上,系统会先从内存中读缓存,缓存没有再从云存储读。

      • 两者在性能、使用行上基本都差不多,无非就是后续可扩展性及安全性,企业版考虑的更多。

      • 网站做好后可以点击网站管理后台中的 生成整站 ,即可一键生成网站所有的 html 静态页面。

      • 网站访问

      安全

      • 数据、附件等都在你自己服务器或者相关华为云阿里云账户上,数据都在手里!不少老板的心里,数据自己掌握着心里头才是安全的,我们系统在这方面让你安心。

      • 系统完全独立运行,不受我们控制。我们万一哪天一不小心倒闭了,没事,您安装的私有SAAS云建站不受影响,你是独立的。(有的单位像是油田,是不开外网的,纯粹内网访问,支撑无外网环境的正常使用,足以证明其完全的独立)

      • 安防检测-网站分离。在某些场景,如政府单位,会定期进行安防检测,本系统可以将 网站访问-后端管理 完全分离独立,管理后台进行了什么设置,MQ推送通知网站访问服务器进行网站更新,而网站访问服务器,就只有固定的html、及 sitemap.xml 等访问请求可进入,从入口层就对安全进行保障。(这种的是需要我们介入进行协助部署)

      • 备份还原。可对模板进行备份及还原操作,改动某个模板时,可以先导出一个备份,如果改错了,还可以通过备份,有选择的将某个模板页进行还原回原本正常的样子。

      • 系统开源,可用于商业用途!但开源版本的我们网站管理后台左下角的标识要带着,至于所做的网站,访问看到的网站不需要放置我方任何标识。多么宽松的条件。

      快速出网站

      • 快速做网站。开通网站-登录网站管理后台-选好模板-改改文字图片-绑定域名-上线 ,你完全可以不用管服务器、模板html代码,将时间用在正确的地方。

      • 快速复制网站。内置网站模板导出导入功能,你做好的网站,可以快速复制同样的出来上线交付

      • 对系统的所有操作、网站访问、是哪个人进行的操作等,都会进行详细记录。以便有异常时可以对其分析、追踪、及精准统计(需要配合ES使用,ElasticSearch云模块价格不菲,一个月三百多)

      高效

      • 网站生成静态html页面,当打开网站时,直接显示的静态html页面,不需要服务器处理什么耗时逻辑运算。

      • 配套软件 扒网站工具 https://gitee.com/mail_osc/templatespider 看好哪个网站,自动扒下来做成模版。所见网站,皆可为我所用

      可扩展及功能定制

      • 开放式模板机制,同帝国CMS、织梦CMS的模板方式,网站想怎么显示就能怎么写html,同时有完善的模板开发辅助软件、插件、及文档。

      • 成熟的插件机制,有数十种扩展插件可直接拿来使用或看其源代码参考,同时有完善的插件开发示例及说明、二次开发文档可供参考 (wm.zvo.cn)

      部分截图

      最后,想学习这个项目的可以查看项目地址:

      • https://gitee.com/mail_osc/wangmarket

    • Controller层代码就该这么写,简洁又优雅!

      作者:gelald
      来源:juejin.cn/post/7123091045071454238

      说到 Controller,相信大家都不陌生,它可以很方便地对外提供数据接口。它的定位,我认为是「不可或缺的配角」,说它不可或缺是因为无论是传统的三层架构还是现在的COLA架构,Controller 层依旧有一席之地,说明他的必要性;说它是配角是因为 Controller 层的代码一般是不负责具体的逻辑业务逻辑实现,但是它负责接收和响应请求

      从现状看问题

      Controller 主要的工作有以下几项

      • 接收请求并解析参数
      • 调用 Service 执行具体的业务代码(可能包含参数校验)
      • 捕获业务逻辑异常做出反馈
      • 业务逻辑执行成功做出响应
      //DTO
      @Data
      public class TestDTO {
          private Integer num;
          private String type;
      }


      //Service
      @Service
      public class TestService {

          public Double service(TestDTO testDTO) throws Exception {
              if (testDTO.getNum() 0) {
                  throw new Exception("输入的数字需要大于0");
              }
              if (testDTO.getType().equals("square")) {
                  return Math.pow(testDTO.getNum(), 2);
              }
              if (testDTO.getType().equals("factorial")) {
                  double result = 1;
                  int num = testDTO.getNum();
                  while (num > 1) {
                      result = result * num;
                      num -= 1;
                  }
                  return result;
              }
              throw new Exception("未识别的算法");
          }
      }


      //Controller
      @RestController
      public class TestController {

          private TestService testService;

          @PostMapping("/test")
          public Double test(@RequestBody TestDTO testDTO) {
              try {
                  Double result = this.testService.service(testDTO);
                  return result;
              } catch (Exception e) {
                  throw new RuntimeException(e);
              }
          }

          @Autowired
          public DTOid setTestService(TestService testService) {
              this.testService = testService;
          }
      }

      如果真的按照上面所列的工作项来开发 Controller 代码会有几个问题

      1. 参数校验过多地耦合了业务代码,违背单一职责原则
      2. 可能在多个业务中都抛出同一个异常,导致代码重复
      3. 各种异常反馈和成功响应格式不统一,接口对接不友好

      改造 Controller 层逻辑

      统一返回结构

      统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此),使用一个状态码、状态信息就能清楚地了解接口调用情况

      //定义返回数据结构
      public interface IResult {
          Integer getCode();
          String getMessage();
      }

      //常用结果的枚举
      public enum ResultEnum implements IResult {
          SUCCESS(2001"接口调用成功"),
          VALIDATE_FAILED(2002"参数校验失败"),
          COMMON_FAILED(2003"接口调用失败"),
          FORBIDDEN(2004"没有权限访问资源");

          private Integer code;
          private String message;

          //省略get、set方法和构造方法
      }

      //统一返回数据结构
      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      public class ResultT> {
          private Integer code;
          private String message;
          private T data;

          public static  Result success(T data) {
              return new Result(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
          }

          public static  Result success(String message, T data) {
              return new Result(ResultEnum.SUCCESS.getCode(), message, data);
          }

          public static Result> failed() {
              return new Result(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
          }

          public static Result> failed(String message) {
              return new Result(ResultEnum.COMMON_FAILED.getCode(), message, null);
          }

          public static Result> failed(IResult errorResult) {
              return new Result(errorResult.getCode(), errorResult.getMessage(), null);
          }

          public static  Result instance(Integer code, String message, T data) {
              Result result = new Result();
              result.setCode(code);
              result.setMessage(message);
              result.setData(data);
              return result;
          }
      }

      统一返回结构后,在 Controller 中就可以使用了,但是每一个 Controller 都写这么一段最终封装的逻辑,这些都是很重复的工作,所以还要继续想办法进一步处理统一返回结构

      统一包装处理

      Spring 中提供了一个类 ResponseBodyAdvice ,能帮助我们实现上述需求

      ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。那这样就可以把统一包装的工作放到这个类里面。

      public interface ResponseBodyAdviceT> {
          boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType);

          @Nullable
          beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
      }

      • supports:判断是否要交给 beforeBodyWrite 方法执行,ture:需要;false:不需要
      • beforeBodyWrite:对 response 进行具体的处理
      // 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成
      @RestControllerAdvice(basePackages = "com.example.demo")
      public class ResponseAdvice implements ResponseBodyAdviceObject> {
          @Override
          public boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType) {
              // 如果不需要进行封装的,可以添加一些校验手段,比如添加标记排除的注解
              return true;
          }
        

          @Override
          public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
              // 提供一定的灵活度,如果body已经被包装了,就不进行包装
              if (body instanceof Result) {
                  return body;
              }
              return Result.success(body);
          }
      }

      经过这样改造,既能实现对 Controller 返回的数据进行统一包装,又不需要对原有代码进行大量的改动

      处理 cannot be cast to java.lang.String 问题

      如果直接使用 ResponseBodyAdvice,对于一般的类型都没有问题,当处理字符串类型时,会抛出 xxx.包装类 cannot be cast to java.lang.String 的类型转换的异常

      ResponseBodyAdvice 实现类中 debug 发现,只有 String 类型的 selectedConverterType 参数值是 org.springframework.http.converter.StringHttpMessageConverter,而其他数据类型的值是 org.springframework.http.converter.json.MappingJackson2HttpMessageConverter

      • String 类型
      • 其他类型 (如 Integer 类型)

      现在问题已经较为清晰了,因为我们需要返回一个 Result 对象

      所以使用 MappingJackson2HttpMessageConverter 是可以正常转换的

      而使用 StringHttpMessageConverter 字符串转换器会导致类型转换失败

      现在处理这个问题有两种方式

      1. beforeBodyWrite 方法处进行判断,如果返回值是 String 类型就对 Result 对象手动进行转换成 JSON 字符串,另外方便前端使用,最好在 @RequestMapping 中指定 ContentType
      @RestControllerAdvice(basePackages = "com.example.demo")
      public class ResponseAdvice implements ResponseBodyAdviceObject> {
          ...
          @Override
          public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
              // 提供一定的灵活度,如果body已经被包装了,就不进行包装
              if (body instanceof Result) {
                  return body;
              }
              // 如果返回值是String类型,那就手动把Result对象转换成JSON字符串
              if (body instanceof String) {
                  try {
                      return this.objectMapper.writeValueAsString(Result.success(body));
                  } catch (JsonProcessingException e) {
                      throw new RuntimeException(e);
                  }
              }
              return Result.success(body);
          }
          ...
      }

      @GetMapping(value = "/returnString", produces = "application/json; charset=UTF-8")
      public String returnString() {
          return "success";
      }
      1. 修改 HttpMessageConverter 实例集合中 MappingJackson2HttpMessageConverter 的顺序。因为发生上述问题的根源所在是集合中 StringHttpMessageConverter 的顺序先于 MappingJackson2HttpMessageConverter 的,调整顺序后即可从根源上解决这个问题
      • 网上有不少做法是直接在集合中第一位添加 MappingJackson2HttpMessageConverter
      @Configuration
      public class WebConfiguration implements WebMvcConfigurer {
          
          @Override
          public void configureMessageConverters(List> converters) {
              converters.add(0new MappingJackson2HttpMessageConverter());
          }
      }

      • 诚然,这种方式可以解决问题,但其实问题的根源不是集合中缺少这一个转换器,而是转换器的顺序导致的,所以最合理的做法应该是调整 MappingJackson2HttpMessageConverter 在集合中的顺序
      @Configuration
      public class WebMvcConfiguration implements WebMvcConfigurer {

          /**
           * 交换MappingJackson2HttpMessageConverter与第一位元素
           * 让返回值类型为String的接口能正常返回包装结果
           *
           * @param converters initially an empty list of converters
           */

          @Override
          public void configureMessageConverters(List> converters) {
              for (int i = 0; i             if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
                      MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = (MappingJackson2HttpMessageConverter) converters.get(i);
                      converters.set(i, converters.get(0));
                      converters.set(0, mappingJackson2HttpMessageConverter);
                      break;
                  }
              }
          }
      }

      参数校验

      Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validationspring validation 是对其的二次封装,常用于 SpringMVC 的参数自动校验,参数校验的代码就不需要再与业务逻辑代码进行耦合了

      @PathVariable 和 @RequestParam 参数校验

      Get 请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参

      对 @PathVariable 和 @RequestParam 参数进行校验需要在入参声明约束的注解

      如果校验失败,会抛出 MethodArgumentNotValidException 异常

      @RestController(value = "prettyTestController")
      @RequestMapping("/pretty")
      @Validated
      public class TestController {

          private TestService testService;

          @GetMapping("/{num}")
          public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num) {
              return num * num;
          }

          @GetMapping("/getByEmail")
          public TestDTO getByAccount(@RequestParam @NotBlank @Email String email) {
              TestDTO testDTO = new TestDTO();
              testDTO.setEmail(email);
              return testDTO;
          }

          @Autowired
          public void setTestService(TestService prettyTestService) {
              this.testService = prettyTestService;
          }
      }

      校验原理

      在 SpringMVC 中,有一个类是 RequestResponseBodyMethodProcessor ,这个类有两个作用(实际上可以从名字上得到一点启发)

      1. 用于解析 @RequestBody 标注的参数
      2. 处理 @ResponseBody 标注方法的返回值

      解析 @RequestBoyd 标注参数的方法是 resolveArgument

      public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
            /**
           * Throws MethodArgumentNotValidException if validation fails.
           * @throws HttpMessageNotReadableException if {@link RequestBody#required()}
           * is {@code true} and there is no body content or if there is no suitable
           * converter to read the content with.
           */

          @Override
          public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
              NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
       throws Exception 
      {

            parameter = parameter.nestedIfOptional();
            //把请求数据封装成标注的DTO对象
            Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
            String name = Conventions.getVariableNameForParameter(parameter);

            if (binderFactory != null) {
              WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
              if (arg != null) {
                //执行数据校验
                validateIfApplicable(binder, parameter);
                //如果校验不通过,就抛出MethodArgumentNotValidException异常
                //如果我们不自己捕获,那么最终会由DefaultHandlerExceptionResolver捕获处理
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                  throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
              }
              if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
              }
            }

            return adaptArgumentIfNecessary(arg, parameter);
          }
      }

      public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
        /**
          * Validate the binding target if applicable.
          * 

      The default implementation checks for {@code @javax.validation.Valid},
          * Spring's {@link org.springframework.validation.annotation.Validated},
          * and custom annotations whose name starts with "Valid".
          * @param binder the DataBinder to be used
          * @param parameter the method parameter descriptor
          * @since 4.1.5
          * @see #isBindExceptionRequired
          */


         protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
          //获取参数上的所有注解
            Annotation[] annotations = parameter.getParameterAnnotations();
            for (Annotation ann : annotations) {
            //如果注解中包含了@Valid、@Validated或者是名字以Valid开头的注解就进行参数校验
               Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
               if (validationHints != null) {
              //实际校验逻辑,最终会调用Hibernate Validator执行真正的校验
              //所以Spring Validation是对Hibernate Validation的二次封装
                  binder.validate(validationHints);
                  break;
               }
            }
         }
      }

      @RequestBody 参数校验

      Post、Put 请求的参数推荐使用 @RequestBody 请求体参数

      对 @RequestBody 参数进行校验需要在 DTO 对象中加入校验条件后,再搭配 @Validated 即可完成自动校验

      如果校验失败,会抛出 ConstraintViolationException 异常

      //DTO
      @Data
      public class TestDTO {
          @NotBlank
          private String userName;

          @NotBlank
          @Length(min = 6, max = 20)
          private String password;

          @NotNull
          @Email
          private String email;
      }

      //Controller
      @RestController(value = "prettyTestController")
      @RequestMapping("/pretty")
      public class TestController {

          private TestService testService;

          @PostMapping("/test-validation")
          public void testValidation(@RequestBody @Validated TestDTO testDTO) {
              this.testService.save(testDTO);
          }

          @Autowired
          public void setTestService(TestService testService) {
              this.testService = testService;
          }
      }

      校验原理

      声明约束的方式,注解加到了参数上面,可以比较容易猜测到是使用了 AOP 对方法进行增强

      而实际上 Spring 也是通过 MethodValidationPostProcessor 动态注册 AOP 切面,然后使用 MethodValidationInterceptor 对切点方法进行织入增强

      public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {
        
          //指定了创建切面的Bean的注解
         private Class extends Annotation> validatedAnnotationType = Validated.class;
        
          @Override
          public void afterPropertiesSet() {
              //为所有@Validated标注的Bean创建切面
              Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
              //创建Advisor进行增强
              this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
          }

          //创建Advice,本质就是一个方法拦截器
          protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
              return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
          }
      }

      public class MethodValidationInterceptor implements MethodInterceptor {
          @Override
          public Object invoke(MethodInvocation invocation) throws Throwable {
              //无需增强的方法,直接跳过
              if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
                  return invocation.proceed();
              }
            
              Class>[] groups = determineValidationGroups(invocation);
              ExecutableValidator execVal = this.validator.forExecutables();
              Method methodToValidate = invocation.getMethod();
              Set> result;
              try {
                  //方法入参校验,最终还是委托给Hibernate Validator来校验
                   //所以Spring Validation是对Hibernate Validation的二次封装
                  result = execVal.validateParameters(
                      invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
              }
              catch (IllegalArgumentException ex) {
                  ...
              }
              //校验不通过抛出ConstraintViolationException异常
              if (!result.isEmpty()) {
                  throw new ConstraintViolationException(result);
              }
              //Controller方法调用
              Object returnValue = invocation.proceed();
              //下面是对返回值做校验,流程和上面大概一样
              result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
              if (!result.isEmpty()) {
                  throw new ConstraintViolationException(result);
              }
              return returnValue;
          }
      }

      自定义校验规则

      有些时候 JSR303 标准中提供的校验规则不满足复杂的业务需求,也可以自定义校验规则

      自定义校验规则需要做两件事情

      1. 自定义注解类,定义错误信息和一些其他需要的内容
      2. 注解校验器,定义判定规则
      //自定义注解类
      @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Constraint(validatedBy = MobileValidator.class)
      public @interface Mobile 
      {
          /**
           * 是否允许为空
           */

          boolean required() default true;

          /**
           * 校验不通过返回的提示信息
           */

          String message() default "不是一个手机号码格式";

          /**
           * Constraint要求的属性,用于分组校验和扩展,留空就好
           */

          Class>[] groups() default {};
          Class extends Payload>[] payload() default {};
      }

      //注解校验器
      public class MobileValidator implements ConstraintValidatorMobileCharSequence> {

          private boolean required = false;

          private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$"); // 验证手机号

          /**
           * 在验证开始前调用注解里的方法,从而获取到一些注解里的参数
           *
           * @param constraintAnnotation annotation instance for a given constraint declaration
           */

          @Override
          public void initialize(Mobile constraintAnnotation) {
              this.required = constraintAnnotation.required();
          }

          /**
           * 判断参数是否合法
           *
           * @param value   object to validate
           * @param context context in which the constraint is evaluated
           */

          @Override
          public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
              if (this.required) {
                  // 验证
                  return isMobile(value);
              }
              if (StringUtils.hasText(value)) {
                  // 验证
                  return isMobile(value);
              }
              return true;
          }

          private boolean isMobile(final CharSequence str) {
              Matcher m = pattern.matcher(str);
              return m.matches();
          }
      }

      自动校验参数真的是一项非常必要、非常有意义的工作。 JSR303 提供了丰富的参数校验规则,再加上复杂业务的自定义校验规则,完全把参数校验和业务逻辑解耦开,代码更加简洁,符合单一职责原则。

      自定义异常与统一拦截异常

      原来的代码中可以看到有几个问题

      1. 抛出的异常不够具体,只是简单地把错误信息放到了 Exception 中
      2. 抛出异常后,Controller 不能具体地根据异常做出反馈
      3. 虽然做了参数自动校验,但是异常返回结构和正常返回结构不一致

      自定义异常是为了后面统一拦截异常时,对业务中的异常有更加细颗粒度的区分,拦截时针对不同的异常作出不同的响应

      而统一拦截异常的目的一个是为了可以与前面定义下来的统一包装返回结构能对应上,另一个是我们希望无论系统发生什么异常,Http 的状态码都要是 200 ,尽可能由业务来区分系统的异常

      //自定义异常
      public class ForbiddenException extends RuntimeException {
          public ForbiddenException(String message) {
              super(message);
          }
      }

      //自定义异常
      public class BusinessException extends RuntimeException {
          public BusinessException(String message) {
              super(message);
          }
      }

      //统一拦截异常
      @RestControllerAdvice(basePackages = "com.example.demo")
      public class ExceptionAdvice {

          /**
           * 捕获 {@code BusinessException} 异常
           */

          @ExceptionHandler({BusinessException.class})
          public ResulthandleBusinessException(BusinessException ex
      {
              return Result.failed(ex.getMessage());
          }

          /**
           * 捕获 {@code ForbiddenException} 异常
           */

          @ExceptionHandler({ForbiddenException.class})
          public ResulthandleForbiddenException(ForbiddenException ex
      {
              return Result.failed(ResultEnum.FORBIDDEN);
          }

          /**
           * {@code @RequestBody} 参数校验不通过时抛出的异常处理
           */

          @ExceptionHandler({MethodArgumentNotValidException.class})
          public ResulthandleMethodArgumentNotValidException(MethodArgumentNotValidException ex
      {
              BindingResult bindingResult = ex.getBindingResult();
              StringBuilder sb = new StringBuilder("校验失败:");
              for (FieldError fieldError : bindingResult.getFieldErrors()) {
                  sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
              }
              String msg = sb.toString();
              if (StringUtils.hasText(msg)) {
                  return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
              }
              return Result.failed(ResultEnum.VALIDATE_FAILED);
          }

          /**
           * {@code @PathVariable} 和 {@code @RequestParam} 参数校验不通过时抛出的异常处理
           */

          @ExceptionHandler({ConstraintViolationException.class})
          public ResulthandleConstraintViolationException(ConstraintViolationException ex
      {
              if (StringUtils.hasText(ex.getMessage())) {
                  return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
              }
              return Result.failed(ResultEnum.VALIDATE_FAILED);
          }

          /**
           * 顶级异常捕获并统一处理,当其他异常无法处理时候选择使用
           */

          @ExceptionHandler({Exception.class})
          public Resulthandle(Exception ex
      {
              return Result.failed(ex.getMessage());
          }

      }

      总结

      做好了这一切改动后,可以发现 Controller 的代码变得非常简洁,可以很清楚地知道每一个参数、每一个 DTO 的校验规则,可以很明确地看到每一个 Controller 方法返回的是什么数据,也可以方便每一个异常应该如何进行反馈

      这一套操作下来后,我们能更加专注于业务逻辑的开发,代码简洁、功能完善,何乐而不为呢?