Несколько месяцев назад, во время работы над одним из проектов компании, нам нужно было разработать службы REST, которые используются для отправки электронной почты в зависимости от данных, отправляемых клиентским приложением. Во время разработки этого сервиса мы решили создать простой механизм рабочих процессов, который будет взимать плату за отправку электронной почты, но также этот механизм может использоваться для любых простых потоков.
В этой статье я объясню шаг за шагом, как вы можете реализовать свой простой механизм рабочего процесса, который может обрабатывать поток последовательности.
Для реализации этого механизма рабочего процесса мы использовали Spring Framework, но идея о том, как это реализовать, должна быть одинаковой с любым фреймворком, если вы его используете, или без него.
Мы начнем с краткого введения в шаблон последовательности рабочих процессов, после этого мы рассмотрим необходимые интерфейсы и в конце начнем с реализации механизма рабочих процессов с пружиной.
Образец последовательности операций
Последовательность шаблонов рабочих процессов описывает рабочие процессы, в которых каждый шаг (действие) выполняется шаг за шагом, один за другим. На следующем изображении вы можете увидеть, как это должно выглядеть:
Каждое действие, которое будет обрабатываться внутри потока, имеет общий контекст, что позволяет участникам потока обмениваться информацией друг с другом. Идея общего контекста используется, потому что каждый шаг должен быть независимым друг от друга, и они должны быть легко добавлены как часть некоторого другого потока.
Если вы хотите получить больше информации о шаблоне последовательности операций, вы можете посетить: Шаблон последовательности .
Определение необходимого интерфейса
Следующим шагом является создание набора интерфейсов, которые позволяют нам легко создавать рабочие процессы и определять действия рабочих процессов.
Мы можем начать с интерфейса Workflow. Этот интерфейс отвечает за обработку действий рабочего процесса и фактически определяет, что должен делать наш механизм рабочего процесса. Это действительно простой интерфейс с одним методом processWorkflow.
Этот метод вызывается механизмом рабочего процесса и используется для обеспечения рабочего процесса начальными объектами, которые могут использоваться внутри рабочего процесса, и он представляет собой отправную точку каждого рабочего процесса.
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
package ba.codecentric.workflow; import java.util.Map; /** * Process email workflow. * * @author igor.madjeric * */ public interface Workflow { /** * Method for processing workflow. * * @param parameters * maps of object which are needed for workflow processing * @return true in case that workflow is done without errors otherwise false */ public boolean processWorkflow(Map<String, Object> parameters); } Next what we need is interface used for defining workflow action. This is also simple interface whit only one method too. package ba.codecentric.workflow; /** * Define workflow action * * @author igor.madjeric * */ public interface WorkflowAction { /** * Execute action. * * @param context * @throws Exception */ public void doAction(Context context) throws Exception; } So this interface define only doAction method which will be called by workflow implementation. Last interface which we need to define is Context interface . This interface define two methods, one for setting object in context and another for retrieving it. package ba.codecentric.workflow; /** * Context interface. * * Class which extend this interface should be able to provide mechanism for keeping object in context.<br /> * So they can be shared between action inside workflow. * * @author igor.madjeric * */ public interface Context { /** * Set value with specified name in context. * If value already exist it should overwrite value with new one. * * @param name of attribute * @param value which should be stored for specified name */ public void setAttribute(String name, Object value); /** * Retrieve object with specified name from context, * if object does not exists in context it will return null. * * @param name of attribute which need to be returned * @return Object from context or null if there is no value assigned to specified name */ public Object getAttribute(String name); } |
Это все интерфейсы, которые нам нужно определить для нашего простого рабочего процесса
Реализация простого механизма документооборота
После того, как мы определили интерфейсы, мы можем начать с реализации механизма рабочего процесса. Существует ряд требований, которые должен делать двигатель.
Этот механизм должен поддерживать последовательность операций, что означает, что действия выполняются один за другим.
Кроме того, двигатель должен быть способен прецессировать более одного потока.
Действие рабочего процесса должно быть в состоянии обмениваться информацией друг с другом.
Как мы видим, требований не так много, поэтому мы должны начать с его реализации.
Прежде всего мы можем создать класс контекста, который будет использоваться для обработки информации между действиями. Этот класс реализует интерфейс Context и не делает много других вещей.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
package ba.codecentric.workflow.impl; import java.util.HashMap; import java.util.Map; import ba.codecentric.workflow.Context; /** * Save states between different workflow action. * * @author igor.madjeric * */ public class StandardContext implements Context { private Map<String, Object> context; /** * Create context object based. * * @param parameters */ public StandardContext(Map<String, Object> parameters) { if (parameters == null ) { this .context = new HashMap<String, Object>(); } else { this .context = parameters; } } @Override public Object getAttribute(String name) { return context.get(name); } @Override public void setAttribute(String name, Object value) { context.put(name, value); } } |
Второй шаг — создание класса, реализующего интерфейс Workflow. Мы назвали этот класс StandardWorkflow. Помимо реализации интерфейса Workflow этот класс также реализует интерфейс ApplicationContextAware из-за необходимости доступа к репозиторию bean-компонентов. Если вы не используете пружину, вам не нужно это реализовывать.
Мы уже говорили, что рабочий процесс должен поддерживать более одного потока.
Таким образом, действие одного рабочего процесса может быть определено как список, и каждому из этого списка должно быть присвоено какое-то логическое имя. Поэтому для регистрации действий мы можем использовать что-то вроде Map <String, List <WorkflowAction >>. Сначала мы увидим определение bean-компонента SpringWorkflow и одного пользовательского потока, а затем увидим реализацию StandardWorkflow.
Определение компонента StandardWorkflow:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
< bean id = 'standardWorkflow' class = 'de.codecentric.oev.external.services.workflow.standard.StandardWorkflow' > < property name = 'workflowActions' > < map > <!-- <entry key='<CID>_action'><ref bean='<CID>_action'/></entry>--> <!-- OEVBS --> < entry key = 'action1_action' > < ref bean = 'action1_action' /> </ entry > <!-- PVN --> < entry key = 'action2_action' > < ref bean = 'action2_action' /> </ entry > <!-- WPV --> < entry key = 'action3_action' > < ref bean = 'action3_action' /> </ entry > </ map > </ property > </ bean > |
Из этого определения компонента мы видим, что мы определяем действие для каждого клиента, а список действий определяется в ссылочных компонентах.
Вот пример одного из этих компонентов:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
< bean id = 'action1_action' class = 'java.util.ArrayList' > < constructor-arg > <!-- List of Actions --> < list value-type = 'ba.codecentric.workflow.WorkflowAction' > < ref local = 'createEmailAction' /> < ref bean = 'sendEmailAction' /> </ list > </ constructor-arg > </ bean > |
Теперь мы можем видеть, как выглядит StandardWorkflow:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
package ba.codecentric.workflow.impl; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import ba.codecentric.workflow.Context; import ba.codecentric.workflow.Workflow; import ba.codecentric.workflow.WorkflowAction; /** * Define standard workflow for sending email. * * @see Workflow * * @author igor.madjeric * */ public class StandardWorkflow implements Workflow, ApplicationContextAware { private final Log LOG = LogFactory.getLog(StandardWorkflow. class ); private static final String ACTION = 'action' ; private Map<String, List<WorkflowAction>> workflowActions; private ApplicationContext applicationContext; /** *@see de.codecentric.oev.external.services.workflow.Workflow#processWorkflow(java.util.Map) */ @Override public boolean processWorkflow(String workflofName, Map<String, Object> parameters) { Context context = new StandardContext(parameters); List<WorkflowAction> actions = getWorkflowActions(workflofName); for (WorkflowAction action : actions) { try { action.doAction(context); } catch (Exception e) { StringBuilder message = new StringBuilder( 'Failed to complete action:' + action.toString()); message.append( '\n' ); message.append(e.getMessage()); LOG.error(message.toString()); return false ; } } return true ; } private List<WorkflowAction> getWorkflowActions(String actionName) { List<WorkflowAction> actions = workflowActions.get(actionName); if (actions == null || actions.isEmpty()) { LOG.error( 'There is no defined action for ' + actionName); throw new IllegalArgumentException( 'There is no defined action for ' + actionName); } return actions; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException{ this .applicationContext = applicationContext; } // Getter/Setter public Map<String, List<WorkflowAction>> getWorkflowActions() { return workflowActions; } public void setWorkflowActions( Map<String, List<WorkflowAction>> workflowActions) { this .workflowActions = workflowActions; } } |
Опять же, как вы можете видеть, это также простой класс, вся работа выполняется в методе processWorkflow, которому мы предоставляем имя потока и входные параметры. Этот метод создает контекст с указанным параметром, после чего он пытается загрузить действия, определенные для указанного потока, и, если есть поток с указанным именем, он запускает поток.
Как начать поток
Это зависит от ваших потребностей. Вы можете использовать остальные службы, такие как мы, или использовать любой другой механизм, такой как MBeans, запланированные задания, или вы можете сделать прямой вызов из некоторых ваших служб. Все, что вам нужно сделать, это вызвать метод processWorkflow.
Ссылка: Simple Workflow Engine With Spring от нашего партнера по JCG Игоря Маджерика в блоге Игоря Маджерика .