Activiti工作流學習筆記(四)——工作流引擎中責任鏈模式的建立與應用原理

朱季謙 發表於 2021-04-09

原創/朱季謙

本文需要一定責任鏈模式的基礎,主要分成三部分講解:

一、簡單理解責任鏈模式概念
二、Activiti工作流裡責任鏈模式的建立
三、Activiti工作流裡責任鏈模式的應用


一、簡單理解責任鏈模式概念

網上關於責任鏈模式的介紹很多,菜鳥教程上是這樣說的:責任鏈模式(Chain of Responsibility Pattern)為請求建立了一個接收者物件的鏈。在這種模式中,通常每個接收者都包含對另一個接收者的引用。如果一個物件不能處理該請求,那麼它會把相同的請求傳給下一個接收者,依此類推。

這個概念術語比較抽象。

我曾經在 深入理解Spring Security授權機制原理 一文中提到Spring Security在授權過程中有使用到過濾器的概念,過濾器鏈就像一條鐵鏈,中間的每個過濾器都包含對另一個過濾器的引用,從而把相關的過濾器連結起來,像一條鏈的樣子。這時請求執行緒就如螞蟻一樣,會沿著這條鏈一直爬過去-----即,通過各過濾器呼叫另一個過濾器引用方法chain.doFilter(request, response),實現一層巢狀一層地將請求傳遞下去,當該請求傳遞到能被處理的的過濾器時,就會被處理,處理完成後轉發返回。通過過濾器鏈,可實現在不同的過濾器當中對請求request做處理,且過濾器之間彼此互不干擾。

整個流程大致如下:

image

這個過濾器鏈的概念,其實就是責任鏈設計模式在Spring Security中的體現。

摘錄一段網上關於職責鏈模式介紹,其主要包含以下角色:

  1. 抽象處理者(Handler)角色:定義一個處理請求的介面,包含抽象處理方法和一個後繼連線。
  2. 具體處理者(Concrete Handler)角色:實現抽象處理者的處理方法,判斷能否處理本次請求,如果可以處理請求則處理,否則將該請求轉給它的後繼者。
  3. 客戶類(Client)角色:建立處理鏈,並向鏈頭的具體處理者物件提交請求,它不關心處理細節和請求的傳遞過程。

二、Activiti工作流裡責任鏈模式的建立

最近在研究Activiti工作流框架,發現其所有實現都是採用命令模式實現,而命令模式當中的Invoker角色又是採用攔截器鏈式模式,即類似上面提到的過濾器鏈,即設計模式裡的責任鏈模式。

這裡的Activiti工作流版本是6.0。

CommandInterceptor是一個攔截器介面,包含三個方法:

  • setNext()方法是在初始化時,設定每個攔截器物件中包含了下一個攔截器物件,最後形成一條攔截器鏈;
  • getNext()可在每個攔截器物件中呼叫下一個攔截器物件;
  • execute()是每個攔截器對請求的處理。若在上一個攔截器鏈式裡不能處理該請求話,就會通過next.execute(CommandConfig var1, Command var2)將請求傳遞到下一個攔截器做處理,類似上面過濾器裡呼叫下一個過濾器的chain.doFilter(request, response)方法,將請求進行傳遞;
public interface CommandInterceptor {
    <T> T execute(CommandConfig var1, Command<T> var2);

    CommandInterceptor getNext();

    void setNext(CommandInterceptor var1);
}

抽象類AbstractCommandInterceptor實現了CommandInterceptor攔截器介面,在責任鏈模式當中充當抽象處理者(Handler)角色。該類最主要的屬性是 protected CommandInterceptor next,在同一包下,直接通過next即可呼叫下一個攔截器物件。

public abstract class AbstractCommandInterceptor implements CommandInterceptor {
    protected CommandInterceptor next;
    public AbstractCommandInterceptor() {
    }
    public CommandInterceptor getNext() {
        return this.next;
    }
    public void setNext(CommandInterceptor next) {
        this.next = next;
    }
}

接下來,將會分析攔截器鏈是如何初始化與工作的。

SpringBoot整合Activiti配置如下:

  @Configuration
  public class SpringBootActivitiConfig {
      @Bean
      public ProcessEngine processEngine() {
          ProcessEngineConfiguration pro = ProcessEngineConfiguration.createStandaloneProcessEngineConfiguration();
          pro.setJdbcDriver("com.mysql.jdbc.Driver");
          pro.setJdbcUrl("xxxx");
          pro.setJdbcUsername("xxxx");
          pro.setJdbcPassword("xxx");
          //避免釋出的圖片和xml中文出現亂碼
          pro.setActivityFontName("宋體");
          pro.setLabelFontName("宋體");
          pro.setAnnotationFontName("宋體");
          //資料庫更更新策略
          pro.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
          return pro.buildProcessEngine();
      }
  }

這時,啟動專案後,pro.buildProcessEngine()這行程式碼會初始化Activiti框架,進入裡面,會發現它有三種實現,預設是第二種,即ProcessEngineConfigurationImpl。

image

點進去,Activiti框架具體構建buildProcessEngine方法如下,其中 this.init()的作用是環境初始化,包括配置設定、JDBC連線、bean裝載等的:

public ProcessEngine buildProcessEngine() {
    this.init();
    ProcessEngineImpl processEngine = new ProcessEngineImpl(this);
    if (this.isActiviti5CompatibilityEnabled && this.activiti5CompatibilityHandler != null) {
        Context.setProcessEngineConfiguration(processEngine.getProcessEngineConfiguration());
        this.activiti5CompatibilityHandler.getRawProcessEngine();
    }

    this.postProcessEngineInitialisation();
    return processEngine;
}

在this.init()方法裡,涉及到責任鏈模式初始化的方法是this.initCommandExecutors(),裡面詳情如下:

public void initCommandExecutors() {
    this.initDefaultCommandConfig();
    this.initSchemaCommandConfig();
    //初始化命令呼叫器
    this.initCommandInvoker();
    //List存放進涉及到的攔截器
    this.initCommandInterceptors();
    //初始化命令執行器
    this.initCommandExecutor();
}

這裡只需要關注最後三個方法——

  1. this.initCommandInvoker()

    initCommandInvoker()初始化構建了一個CommandInvoker攔截器,它繼承上邊提到的攔截器抽象類AbstractCommandInterceptor。這個攔截器在整條過濾器鏈中是最重要和關鍵,它排在了整條鏈的最後,其實,它才是最終執行請求的,前邊幾個攔截器都是傳遞請求而已。

    public void initCommandInvoker() {
        if (this.commandInvoker == null) {
            if (this.enableVerboseExecutionTreeLogging) {
                this.commandInvoker = new DebugCommandInvoker();
            } else {
                //初始化執行該行程式碼
                this.commandInvoker = new CommandInvoker();
            }
        }
    }
    

    這裡 new CommandInvoker()一個物件,然後將地址複製給this.commandInvoker物件引用,注意,該引用將會用在接下來的initCommandInterceptors()方法裡——

  2. this.initCommandInterceptors();

    initCommandInterceptors方法主要作用是建立一個List集合,然後將需要用到的攔截器都儲存到該List集合裡——

    public void initCommandInterceptors() {
        if (this.commandInterceptors == null) {
            this.commandInterceptors = new ArrayList();
            if (this.customPreCommandInterceptors != null) {
                //使用者自定義前置攔截器
                this.commandInterceptors.addAll(this.customPreCommandInterceptors);
            }
            //框架自帶預設的攔截器
            this.commandInterceptors.addAll(this.getDefaultCommandInterceptors());
            if (this.customPostCommandInterceptors != null) {
                this.commandInterceptors.addAll(this.customPostCommandInterceptors);
            }
            //命令呼叫器,在攔截器鏈最後一個
            this.commandInterceptors.add(this.commandInvoker);
        }
    }
    

    this.getDefaultCommandInterceptors()的程式碼如下:

    public Collection<? extends CommandInterceptor> getDefaultCommandInterceptors() {
        List<CommandInterceptor> interceptors = new ArrayList();
        //日誌攔截器
        interceptors.add(new LogInterceptor());
        CommandInterceptor transactionInterceptor = this.createTransactionInterceptor();
        if (transactionInterceptor != null) {
            interceptors.add(transactionInterceptor);
        }
    	//
        if (this.commandContextFactory != null) {
            interceptors.add(new CommandContextInterceptor(this.commandContextFactory, this));
        }
        //事務攔截器
        if (this.transactionContextFactory != null) {
            interceptors.add(new TransactionContextInterceptor(this.transactionContextFactory));
        }
    
        return interceptors;
    }
    

    可見,方法裡的 this.commandInterceptors 就是一個專門儲存攔截器物件的List集合——

    protected List<CommandInterceptor> commandInterceptors;
    

    這裡只需要重點關注this.commandInterceptors.add(this.commandInvoker)這行程式碼,就是將上邊建立的CommandInvoker攔截器物件儲存到List裡,它是放在initCommandInterceptors()方法最後,某種程度也就意味著,這個攔截器在整條鏈當中處在最後面的位置。

    執行完該this.initCommandInterceptors()方法後,就可獲取到所有的攔截器物件,到這一步時,各攔截器還是互相獨立的,仍無法通過next()來進行呼叫傳遞,那麼,究竟是如何將它們串起來形成一條鏈呢?

    接下來的this.initCommandExecutor()方法,就是實現將各攔截器串起來形成一條長鏈。

  3. this.initCommandExecutor();

該方法有兩個作用,一個是生成Interceptor攔截器鏈,一個是建立命令執行器commandExecutor。

public void initCommandExecutor() {
    if (this.commandExecutor == null) {
        CommandInterceptor first = this.initInterceptorChain(this.commandInterceptors);
        this.commandExecutor = new CommandExecutorImpl(this.getDefaultCommandConfig(), first);
    }
}

this.initInterceptorChain(this.commandInterceptors)是將集合裡的攔截器初始化生成一條攔截器鏈,先迴圈獲取List集合裡的攔截器物件chain.get(i),然後通過setNext()方法在該攔截器物件chain.get(i)裡設定下一個攔截器引用,這樣,就可實現責任鏈裡所謂每個接收者都包含對另一個接收者的引用的功能。

public CommandInterceptor initInterceptorChain(List<CommandInterceptor> chain) {
    if (chain != null && !chain.isEmpty()) {
        for(int i = 0; i < chain.size() - 1; ++i) {
            ((CommandInterceptor)chain.get(i)).setNext((CommandInterceptor)chain.get(i + 1));
        }
        return (CommandInterceptor)chain.get(0);
    } else {
        throw new ActivitiException("invalid command interceptor chain configuration: " + chain);
    }
}

那麼,這條攔截器鏈當中,都有哪些攔截器呢?

直接debug到這裡,可以看到,總共有4個攔截器物件,按照順序排,包括LogInterceptor,CommandContextInterceptor,TransactionContextInterceptor,CommandInvoker(在命令模式裡,該類相當Invoker角色)。這四個攔截器物件在責任鏈模式當中充當了具體處理者(Concrete Handler)角色。

image

責任鏈模式裡剩餘客戶類(Client)角色應該是命令執行器this.commandExecutor。

因此,工作流引擎當中的責任鏈模式結構圖如下:

image

組成一條攔截器鏈如下圖所示——

image

生成攔截器鏈後,會返回一個(CommandInterceptor)chain.get(0),即攔截器LogInterceptor,為什麼只返回第一個攔截器呢,這是一個很巧妙的地方,因為該攔截器裡已經一層一層地巢狀進其他攔截器了,因此,只需要返回第一個攔截器,賦值給first即可。

接下來,就會建立命令執行器——

this.commandExecutor = new CommandExecutorImpl(this.getDefaultCommandConfig(), first);

這個命令執行器是整個引擎的底層靈魂,通過它,可以實現責任鏈模式與命令模式——

攔截器鏈初始化介紹完成後,接下來開始介紹攔截器鏈在引擎裡的應用方式。


三、Activiti工作流裡責任鏈模式的應用

Activiti引擎的各操作方法其底層基本都是以命令模式來實現的,即呼叫上面建立的命令執行器this.commandExecutor的execute方法來實現的,例如自動生成28張資料庫表的方法,就是通過命令模式去做具體實現的——

this.commandExecutor.execute(processEngineConfiguration.getSchemaCommandConfig(), new SchemaOperationsProcessEngineBuild());

進入到commandExecutor方法裡,會發現前邊new CommandExecutorImpl(this.getDefaultCommandConfig(), first)建立命令執行器時,已將配置物件和巢狀其他攔截器的LogInterceptor攔截器物件,通過構造器CommandExecutorImpl(CommandConfig defaultConfig, CommandInterceptor first)生成物件時,傳參賦值給了相應的物件屬性,其中first引用指向LogInterceptor,即攔截器鏈上的第一個攔截器——

public class CommandExecutorImpl implements CommandExecutor {
    protected CommandConfig defaultConfig;
    protected CommandInterceptor first;

    public CommandExecutorImpl(CommandConfig defaultConfig, CommandInterceptor first) {
        this.defaultConfig = defaultConfig;
        this.first = first;
    }

    public CommandInterceptor getFirst() {
        return this.first;
    }

    public void setFirst(CommandInterceptor commandInterceptor) {
        this.first = commandInterceptor;
    }

    public CommandConfig getDefaultConfig() {
        return this.defaultConfig;
    }

    public <T> T execute(Command<T> command) {
        return this.execute(this.defaultConfig, command);
    }

    public <T> T execute(CommandConfig config, Command<T> command) {
        return this.first.execute(config, command);
    }
}

當引擎執行this.commandExecutor.execute(xxx,xxx))類似方法時,其實是執行了this.first.execute(config, command)方法,這裡的this.first在構建命令執行器時是通過LogInterceptor傳進來的,因此,執行程式碼其實是呼叫了LogInterceptor內部的execute()方法,也就是說,開始攔截器鏈上的第一個LogInterceptor攔截器傳遞方法execute()請求——

image

進入到攔截器鏈上的第一個攔截器LogInterceptor。

根據其內部程式碼可以看出,這是一個跟日誌有關的攔截器,內部並沒有多少增強功能,只是做了一個判斷是否需要debug日誌列印。若需要,則進行debug列印,若不需要,直接進入到 if (!log.isDebugEnabled()) 為true的作用域內部,進而執行this.next.execute(config, command)用以將請求傳遞給下一個攔截器做處理。

public class LogInterceptor extends AbstractCommandInterceptor {
    private static Logger log = LoggerFactory.getLogger(LogInterceptor.class);
    public LogInterceptor() {
    }
    public <T> T execute(CommandConfig config, Command<T> command) {
        if (!log.isDebugEnabled()) {
            return this.next.execute(config, command);
        } else {
            log.debug("\n");
            log.debug("--- starting {} --------------------------------------------------------", command.getClass().getSimpleName());
            Object var3;
            try {
                var3 = this.next.execute(config, command);
            } finally {
                log.debug("--- {} finished --------------------------------------------------------", command.getClass().getSimpleName());
                log.debug("\n");
            }

            return var3;
        }
    }
}

這裡有一個小地方值得稍微打斷說下,就這個 if (!log.isDebugEnabled())判斷。眾生周知,若整合第三方日誌外掛如logback之類,若其配置裡去除debug的列印,即時程式碼裡 存在log.debug("xxxxx")也不會列印到控制檯,那麼,這裡增加一個判斷 if (!log.isDebugEnabled())是否多次一舉呢?

事實上,這裡並非多此一舉,增加這個判斷,是可以提升程式碼執行效率的。因為log.debug("xxxxx")裡的字串拼接早於log.debug("xxxxx")方法執行的,也就是說,即使該log.debug("xxxxx")不會列印,但其內部的字串仍然會進行拼接,而拼接,是需要時間的,雖然很細微,但同樣屬於影響效能範疇內的。因此,增加一個if判斷,若無需要列印debug日誌時,那麼就無需讓其內部的字串進行自動拼接。

這是一個很小的知識點,但面試過程中其實是有可能會遇到這類與日誌相關的面試題的。

接下來,讓我們繼續回到攔截器鏈的傳遞上來。

LogInterceptor攔截器呼叫this.next.execute(config, command),意味著將請求傳遞到下一個攔截器上進行處理,根據前邊分析,可知下一個攔截器是CommandContextInterceptor,根據程式碼大概可知,這個攔截器內主要是獲取上下文配置物件和資訊相關的,這些都是在工作流引擎初始化時生成的,它們被儲存在Stack棧裡,具體都儲存了哪些資訊暫不展開分析——

public class CommandContextInterceptor extends AbstractCommandInterceptor {
    ......
public <T> T execute(CommandConfig config, Command<T> command) {
    CommandContext context = Context.getCommandContext();
    boolean contextReused = false;
    if (config.isContextReusePossible() && context != null && context.getException() == null) {
        contextReused = true;
        context.setReused(true);
    } else {
        context = this.commandContextFactory.createCommandContext(command);
    }

    try {
        Context.setCommandContext(context);
        Context.setProcessEngineConfiguration(this.processEngineConfiguration);
        if (this.processEngineConfiguration.getActiviti5CompatibilityHandler() != null) {
            Context.setActiviti5CompatibilityHandler(this.processEngineConfiguration.getActiviti5CompatibilityHandler());
        }
        //繼續將命令請求傳遞到下一個攔截器
        Object var5 = this.next.execute(config, command);
        return var5;
    } catch (Exception var31) {
        context.exception(var31);
    } finally {
     ......
    }

    return null;
}
}

CommandContextInterceptor攔截器沒有對命令請求做處理,它繼續將請求傳遞到下一個攔截器TransactionContextInterceptor,根據名字就大概可以猜到,這個攔截器主要是增加與事務有關的功能——

public <T> T execute(CommandConfig config, Command<T> command) {
    CommandContext commandContext = Context.getCommandContext();
    boolean isReused = commandContext.isReused();
    Object var9;
    try {
        if (this.transactionContextFactory != null && !isReused) {
            TransactionContext transactionContext = this.transactionContextFactory.openTransactionContext(commandContext);
            Context.setTransactionContext(transactionContext);
            commandContext.addCloseListener(new TransactionCommandContextCloseListener(transactionContext));
        }
        var9 = this.next.execute(config, command);
    } finally {
    ......
    }
    return var9;
}

TransactionContextInterceptor攔截器同樣沒有對命令請求做處理,而是繼續傳遞到下一個攔截器,也就是最後一個攔截器CommandInvoker,根據名字可以大概得知,這是一個與命令請求有關的攔截器,傳遞過來的請求將會在這個攔截器裡處理——

public class CommandInvoker extends AbstractCommandInterceptor {
    ......
    public <T> T execute(CommandConfig config, final Command<T> command) {
        final CommandContext commandContext = Context.getCommandContext();
        commandContext.getAgenda().planOperation(new Runnable() {
            public void run() {
                commandContext.setResult(command.execute(commandContext));
            }
        });
        this.executeOperations(commandContext);
        if (commandContext.hasInvolvedExecutions()) {
            Context.getAgenda().planExecuteInactiveBehaviorsOperation();
            this.executeOperations(commandContext);
        }
        return commandContext.getResult();
    }
}

進入到其內部,可以發現,這裡沒有再繼續呼叫this.next.execute(config, command)這樣的請求進行傳遞,而是直接執行command.execute(commandContext),然後將返回值進行返回,其中,command是請求引數當中的第二個引數,讓我們回過頭看下該請求案例最開始的呼叫——

this.commandExecutor.execute(processEngineConfiguration.getSchemaCommandConfig(), new SchemaOperationsProcessEngineBuild());

這裡的第二個引數是new SchemaOperationsProcessEngineBuild(),不妨進入到SchemaOperationsProcessEngineBuild類中,是吧,其內部同樣有一個execute方法——

public final class SchemaOperationsProcessEngineBuild implements Command<Object> {
    public SchemaOperationsProcessEngineBuild() {
    }

    public Object execute(CommandContext commandContext) {
        DbSqlSession dbSqlSession = commandContext.getDbSqlSession();
        if (dbSqlSession != null) {
            dbSqlSession.performSchemaOperationsProcessEngineBuild();
        }

        return null;
    }
}

可見,CommandInvoker攔截器內部執行command.execute(commandContext),就相當於執行了new SchemaOperationsProcessEngineBuild().execute(commandContext),也就是——

 public Object execute(CommandContext commandContext) {
        DbSqlSession dbSqlSession = commandContext.getDbSqlSession();
        if (dbSqlSession != null) {
            dbSqlSession.performSchemaOperationsProcessEngineBuild();
        }
        return null;
    }

這是一種命令模式的實現。

本文主要是分析責任鏈模式在Activiti框架中的實踐,故暫不展開分析框架中的其他設計模式,有興趣的童鞋可以自行深入研究,在Activiti框架當中,其操作功能底層基本都是以命令模式來實現的。

至此,就大概分析完了責任鏈模式在Activiti框架的建立和應用,學習完這塊內容,我對責任鏈模式有了更好理解,相對於看網上那些簡單以小例子來介紹設計模式的方法,我更喜歡去深入框架當中學習其設計模式,這更能讓我明白,這種設計模式在什麼場景下適合應用,同時,能潛移默化地影響到我,讓我在設計系統架構時,能明白各設計模式的落地場景具體都是怎樣的。