Integrate Activiti BPM with Spring

      2 Comments on Integrate Activiti BPM with Spring

In this post, you will learn how to integrate Activiti’s engine and REST API into your Spring application. At the same time, you will be able to adapt the Process Engine to your needs by modifying the database connection and the Async Job Executor.

Contribute Code

If you would like to become an active contributor to this project please follow these simple steps:

    1. Fork it
    2. Create your feature branch
    3. Commit your changes
    4. Push to the branch
    5. Create new Pull Request

Source code can be downloaded from github.

What you’ll need

  • About 20 minutes
  • A favorite IDE
  • JDK 7 or later. It can be made to work with JDK6, but it will need configuration tweaks. Please check the Spring Boot documentation
  • An empty Spring project. You can follow the steps from here.

Introduction

Activiti is an open source, light-weight process definition driven and Business Process Management engine. As you will see, it is easy to integrate with any Java technology or project.

A process definition is a typical workflow made up of individual boxes called tasks, which specify what action needs to be performed. It is visualized as a flow-chart-like diagram based on the BPMN 2.0 standard. Thanks to BPMN, business can now understand their business  procedures in a graphical notation.

Activiti’s source code can be downloaded from github. The project was founded and is sponsored by Alfresco and distributed under the Apache license, but enjoys contributions from all across the globe and industries.

Spring Boot Integration

Integrating Activiti’s engine into a Spring microservice is quite easy. Basically, you just need to add the needed dependencies and a database. Within minutes, you can have a production-ready service up and running with the capability to orchestrate human workflows to achieve a certain goal.

Getting Started

Once you have created an empty project and imported into your favorite IDE, it is time to modify the pom.xml file. If you have not created the project yet, you can follow the steps described in here.

Let’s open the pom.xml file, and add the dependencies needed to get Spring Boot, Activiti and a database. We’ll use an H2 in memory database to keep things simple.

First we add the version of Activiti that we will be using as a property. Notice that we will use the latest current version 5.22.0.

<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  <java.version>1.8</java.version>
  <activiti.version>5.22.0</activiti.version>
</properties>

Next, we add two dependencies. But do not remove the previously added ones.

<dependencies>		
  <!-- Activiti BPM workflow engine -->
  <dependency><!-- Activiti Spring Boot Starter Basic -->
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter-basic</artifactId>
    <version>${activiti.version}</version>
  </dependency><!-- Activiti Spring Boot Starter Basic -->
  
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
  </dependency>
</dependencies>

That’s it. Yuo have successfully included Activiti into your project. Now let’s give it a test run. Wait a minute! When you try to execute the project, it fails! How come you ask?! This is the error you are seeing:

Error starting ApplicationContext. To display the auto-configuration report re-run your application with 'debug' enabled.
2017-04-16 22:08:23.599 ERROR 10308 --- [           main] o.s.boot.SpringApplication               : Application startup failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springProcessEngineConfiguration' defined in class path resource [org/activiti/spring/boot/DataSourceProcessEngineAutoConfiguration$DataSourceProcessEngineConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.activiti.spring.SpringProcessEngineConfiguration]: Factory method 'springProcessEngineConfiguration' threw exception; nested exception is java.io.FileNotFoundException: class path resource [processes/] cannot be resolved to URL because it does not exist
  at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:599) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE]
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1173) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE]
  ...
  ...
  ...
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.activiti.spring.SpringProcessEngineConfiguration]: Factory method 'springProcessEngineConfiguration' threw exception; nested exception is java.io.FileNotFoundException: class path resource [processes/] cannot be resolved to URL because it does not exist
  at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:189) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE]
  at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:588) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE]
  ... 17 common frames omitted
Caused by: java.io.FileNotFoundException: class path resource [processes/] cannot be resolved to URL because it does not exist
  at org.springframework.core.io.ClassPathResource.getURL(ClassPathResource.java:187) ~[spring-core-4.3.7.RELEASE.jar:4.3.7.RELEASE]
  at org.springframework.core.io.support.PathMatchingResourcePatternResolver.findPathMatchingResources(PathMatchingResourcePatternResolver.java:463) ~[spring-core-4.3.7.RELEASE.jar:4.3.7.RELEASE]
  at org.springframework.core.io.support.PathMatchingResourcePatternResolver.getResources(PathMatchingResourcePatternResolver.java:292) ~[spring-core-4.3.7.RELEASE.jar:4.3.7.RELEASE]

  ...
  ...
  ...

There is a nice feature that is enabled when Activiti is integrated with Spring. This feature automatically deploys process definitions found under classpath:process/, every time the process engine is created. However, since Activiti’s version 5.19.0.2, if there are no process definitions in that folder, the process engine cannot be started. Give it a shot, change Activiti’s version to 5.19.0 and try to start it. We will change the version to 5.19.0 for now, and later own we will add  process definition and test version 5.22.0 again.

You could already run this application using version 5.19.0. However, it won’t do anything functionally but behind the scenes it already:

  • Created an in-memory H2 database
  • Created an Activiti process engine using that database
  • Exposed all Activiti services as Spring Beans
  • Configured the Activiti async job executor, mail server, etc.

Adding REST Support

Until now, we have embedded the Activiti’s engine into our application. But sometimes we need to we able to allow machine to machine communication (M2M). One way of doing this is by exchanging information via REST messages. So let’s add the following dependencies to our project:

<dependency><!-- Activiti Spring Boot Starter Rest Api -->
  <groupId>org.activiti</groupId>
  <artifactId>activiti-spring-boot-starter-rest-api</artifactId>
  <version>${activiti.version}</version>
</dependency><!-- Activiti Spring Boot Starter Rest Api -->

This dependency takes the Activiti REST API (which is written in Spring MVC) and exposes this fully in our application. For a more detail information about the REST API, please visit Activiti’s user guide.

At this point, if you run the application, you will see all the REST endpoints exposed. However, if you try to do a request, you will get an 401 – Unauthorized response. This is because all REST-resources require a valid Activiti-user to be authenticated by default, but none is automatically created when including the REST API dependency. So, let’s add a valid user to our application, by copying and pasting the below code into our main class.

@Bean
InitializingBean usersAndGroupsInitializer(final IdentityService identityService) {

    return new InitializingBean() {
        public void afterPropertiesSet() throws Exception {

            Group group = identityService.newGroup("user");
            group.setName("users");
            group.setType("security-role");
            identityService.saveGroup(group);

            User admin = identityService.newUser("admin");
            admin.setPassword("admin");
            identityService.saveUser(admin);

        }
    };
}

This code snippet does two things:

  1. It creates the group called users, and sets it type to security-role.
  2. It add a user called admin to the previously created group with the password admin.

Now, do the request again, but remember to include the Basic Authentication in you header, specifying the username and password that was just created. Remember not to include the above code in your production ready application. You do not want to add a back door!

Changing the Data Source

NOTE: In this section, we will be creating a property class called ActivitiDataSourceProperties. Nevertheless, its usage will not be obvious until further in the post, once we reach the section Overriding The SpringProcessEngineConfiguration Bean.

An in-memory database like H2 is good for a testing environment. But once you move the application to a production ready environment, you will probably will be using other type of database like MySQL or Oracle.

To change the database that Activiti must use, simple override the default by providing a data source bean. To do this, we will perform two steps:

  • Modify the data source properties in the application.properties file found in the classpath.
  • Create a data source bean, which will override the default one.

Start by opening the application.properties file found in the classpath and add the following properties. Do not worry, it is probable that the file is completely empty. As these properties are managed by the DataSourceProperties class, Spring enables an auto-complete feature.

  • spring.datasource.driver-class-name: Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.
  • spring.datasource.password: Login password of the database.
  • spring.datasource.url: JDBC url of the database.
  • spring.datasource.username: Login username of the database.

By adding this properties, we will be overriding the default values found in Spring’s DataSourceProperties class.

Second, we need to override the default bean. We can create the new bean in the file where the main function is, for example. Copy and paste the following code:

  @Autowired
  private DataSourceProperties properties;
  
  @Bean(name = "datasource.activiti")
  public DataSource activitiDataSource() {
    return DataSourceBuilder.create(this.properties.getClassLoader())
        .url(this.properties.getUrl())
        .username(this.properties.getUsername())
        .password(this.properties.getPassword())
        .driverClassName(this.properties.getDriverClassName())
            .build();
  }

By autowiring the DataSourceProperties, Spring automatically will read the application.properties file and assigns the values that it finds within the file. In other words, it will look for properties which start with “spring.datasource” and that at the same time it finds a matching getter.

Since we are using a properties class, we need to add an additional dependency:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

Here, we are using the Spring’s helper class DataSourceBuilder, which is a convenience class for building a data source with common implementations and properties.

The data source that is constructed based on the default provided JDBC properties will have the default MyBatis connection pool settings. The following attributes can optionally be set to tweak that connection pool (taken from the MyBatis documentation):

  • jdbcMaxActiveConnections: The number of active connections that the connection pool at maximum at any time can contain. Default is 10.
  • jdbcMaxIdleConnections: The number of idle connections that the connection pool at maximum at any time can contain.
  • jdbcMaxCheckoutTime: The amount of time in milliseconds a connection can be checked out from the connection pool before it is forcefully returned. Default is 20000 (20 seconds).
  • jdbcMaxWaitTime: This is a low level setting that gives the pool a chance to print a log status and re-attempt the acquisition of a connection in the case that it is taking unusually long (to avoid failing silently forever if the pool is misconfigured) Default is 20000 (20 seconds).

By default, these four properties are not exposed in the ActivitiProperties class, and that is why, you do not see them in the application.properties files. So, we will create our own properties class which will have getters and setters for these four properties.

Create a package where are newly property classes will reside. I will called it com.canchitodev.example.configuration.properties. And inside, create a class which name will be ActivitiDataSourceProperties. Once you have all these, copy and paste the below code. Remember that the getters and setters are removed for simplicity.

package com.canchitodev.example.configuration.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix="activiti.datasource")
public class ActivitiDataSourceProperties {
  
  private String url;
  private String username;
  private String password;
  private String driverClassName;
  private Integer jdbcMaxWaitTime=20000;
  private Integer jdbcMaxCheckoutTime=20000;
  private Integer jdbcMaxIdleConnections=10;
  private Integer jdbcMaxActiveConnections=10;
  private Boolean dbEnableEventLogging=true;
  
  // Getters and setters are removed for simplicity
  @Override
  public String toString() {
    return "ActivitiDataSourceProperties [url=" + url + ", username=" + username + ", password=" + password
        + ", driverClassName=" + driverClassName + ", jdbcMaxWaitTime=" + jdbcMaxWaitTime
        + ", jdbcMaxCheckoutTime=" + jdbcMaxCheckoutTime + ", jdbcMaxIdleConnections=" + jdbcMaxIdleConnections
        + ", jdbcMaxActiveConnections=" + jdbcMaxActiveConnections + ", dbEnableEventLogging="
        + dbEnableEventLogging + "]";
  }
}

The annotation @ConfigurationProperties(prefix="activiti.datasource") tells Spring that the class ActivitiDataSourceProperties will be a place holder for properties, and which properties are to be mapped to all those properties found in the application.properties file that start with “activiti.datasource”. As a result, Spring will set the value of the properties:

  • activiti.datasource.url: JDBC URL of the database.
  • activiti.datasource.username: Username to connect to the database.
  • activiti.datasource.password: Password to connect to the database.
  • activiti.datasource.driver-class-name: Implementation of the driver for the specific database type.
  • activiti.datasource.jdbc-max-wait-time: This is a low level setting that gives the pool a chance to print a log status and re-attempt the acquisition of a connection in the case that it is taking unusually long (to avoid failing silently forever if the pool is misconfigured) Default is 20000 (20 seconds).
  • activiti.datasource.jdbc-max-checkout-time: The amount of time in milliseconds a connection can be checked out from the connection pool before it is forcefully returned. Default is 20000 (20 seconds).
  • activiti.datasource.jdbc-max-idle-connections: The number of idle connections that the connection pool at maximum at any time can contain.
  • activiti.datasource.jdbc-max-active-connections: The number of active connections that the connection pool at maximum at any time can contain. Default is 10.

The first 4 will not be used at the moment, but the rest can be copied into the application.properties file and assigned a value.

NOTE: If you would like to learn more about properties, I recommend reading the post “Empowering your apps with Spring Boot’s property support” by Greg Turnquist.

Configuring The Async Job Executor

NOTE: In this section, we will be creating a property class called ActivitiAsycExecutorProperties. Nevertheless, its usage will not be obvious until further in the post, once we reach the section Overriding The SpringProcessEngineConfiguration Bean.

Since version 5.17, Activiti offers two ways for it to execute the jobs. The first one is the Job Executor which is a component that manages a couple of threads to fire timers and also asynchronous messages. By default, it is still used and activated when the process engine starts, but we will be disabling it. If you would like to read more about it, please refer to the advance section of Activiti’s user guide.

The second one is the Async Job Executor. The Async executor is a component that manages a thread pool to fire timers and other asynchronous tasks. Moreover it is a more performance and more database friendly way of executing asynchronous jobs in the Activiti Engine. It’s therefore recommended to switch to the Async executor. By default, the it is not enabled.

IMPORTANT: Only one executor can be enabled, as they both executors deal with timers and asynchronous jobs in the Activiti Engine.

Activiti recommends using the Async Job Executor due to the following advantages:

  • Less database queries because asynchronous jobs are executed without polling the database
  • For non-exclusive jobs there’s no chance to run into OptimisticLockingExceptions anymore
  • Exclusive jobs are now locked at process instance level instead of the cumbersome logic of queuing exclusive jobs in the Job Executor

The Async Job Executor can be configured to meet your needs:

  • corePoolSize: The minimal number of threads that are kept alive in the thread pool for job execution. Default value is 2.
  • maxPoolSize: The maximum number of threads that are kept alive in the thread pool for job execution. Default value is 10.
  • keepAliveTime: The time (in milliseconds) a thread used for job execution must be kept alive before it is destroyed. Default setting is 0. Having a non-default setting of 0 takes resources, but in the case of many job executions it avoids creating new threads all the time. Default value is 5000.
  • queueSize: The size of the queue on which jobs to be executed are placed. Default value is 100.
  • maxTimerJobsPerAcquisition: The number of timer jobs that are fetched from the database in one query. Default value is 1.
  • maxAsyncJobsDuePerAcquisition: The number of asynchronous jobs due that are fetched from the database in one query. Default value is 1.
  • defaultAsyncJobAcquireWaitTimeInMillis: The time in milliseconds between asynchronous job due queries being executed. Default value is 10000.
  • defaultTimerJobAcquireWaitTimeInMillis: The time in milliseconds between timer job queries being executed. Default value is 10000.
  • timerLockTimeInMillis: The time in milliseconds that a timer job is locked before being retried again. The Activiti Engine considers the timer job to have failed after this period of time and will retry. Default value is 300000.
  • asyncJobLockTimeInMillis: The time in milliseconds that an asynchronous job is locked before being retried again. The Activiti Engine considers the asynchronous job to have failed after this period of time and will retry. Default value is 300000.

Inside the package com.canchitodev.example.configuration.properties, create a class which name will be ActivitiAsynExecutorProperties. Once you have all these, copy and paste the below code. Remember that the getters and setters are removed for simplicity.

package com.canchitodev.example.configuration.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix="activiti.async-executor")
public class ActivitiAsycExecutorProperties {
  
  private Integer corePoolSize=2;
  private Integer maxPoolSize=10;
  private Integer keepAliveTime=5000;
  private Integer queueSize=100;
  private Integer maxTimerJobsPerAcquisition=1;
  private Integer maxAsyncJobsDuePerAcquisition=1;
  private Integer defaultAsyncJobAcquireWaitTimeInMillis=10000;
  private Integer defaultTimerJobAcquireWaitTimeInMillis=10000;
  private Integer timerLockTimeInMillis=300000;
  private Integer asyncJobLockTimeInMillis=300000;
  
  // Getters and setters are removed for simplicity
  
  @Override
  public String toString() {
    return "ActivitiAsycExecutorProperties [corePoolSize=" + corePoolSize + ", maxPoolSize=" + maxPoolSize
        + ", keepAliveTime=" + keepAliveTime + ", queueSize=" + queueSize + ", maxTimerJobsPerAcquisition="
        + maxTimerJobsPerAcquisition + ", maxAsyncJobsDuePerAcquisition=" + maxAsyncJobsDuePerAcquisition
        + ", defaultAsyncJobAcquireWaitTimeInMillis=" + defaultAsyncJobAcquireWaitTimeInMillis
        + ", defaultTimerJobAcquireWaitTimeInMillis=" + defaultTimerJobAcquireWaitTimeInMillis
        + ", timerLockTimeInMillis=" + timerLockTimeInMillis + ", asyncJobLockTimeInMillis="
        + asyncJobLockTimeInMillis + "]";
  }
}

The annotation @ConfigurationProperties(prefix="activiti.async-executor") tells Spring that the class ActivitiAsynExecutorProperties will be a place holder for properties, and which properties are to be mapped to all those properties found in the application.properties file that start with “activiti.async-executor”.

NOTE: If you would like to learn more about properties, I recommend reading the post Empowering your apps with Spring Boot’s property support by Greg Turnquist.

Overriding the SpringProcessEngineConfiguration Bean

Now that we have created all the properties that we need, we can now proceed to modifying Activiti’s process engine. For simplicity, we will be doing all these modifications on the class where the main function is. In my case, the class IntegrateActivitiWithSpringApplication found in package com.canchitodev.example is where the main method is found.

@SpringBootApplication
@EnableConfigurationProperties(value={ActivitiAsycExecutorProperties.class, ActivitiDataSourceProperties.class})
public class IntegrateActivitiWithSpringApplication {
  
  @Autowired
  private DataSourceProperties dataSourceproperties;
  
  @Autowired
  private ActivitiAsycExecutorProperties activitiAsycExecutorProperties;
  
  @Autowired
  private ActivitiDataSourceProperties activitiDataSourceProperties;

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

 This fragment of code from the class IntegrateActivitiWithSpringApplication shows a couple of key components:

  • @EnableConfigurationProperties leverages ActivitiAsyncExecutorProperties and ActivitiDataSourceProperties as a source of properties and makes them available to the entire class.
  • We are autowiring ActivitiAsyncExecutorProperties and ActivitiDataSourceProperties so that an instance of them is automatically created.

Further below, we will add the function which will modify the process engine configuration using the properties that we created in section Changing the Data Source and Changing the Async job Executor.

...
...
@Bean
public BeanPostProcessor activitiSpringProcessEngineConfigurer() {
    return new BeanPostProcessor() {

        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            if (bean instanceof SpringProcessEngineConfiguration) {
            	// If it is the SpringProcessEngineConfiguration, we want to add or modify some configuration.
                  SpringProcessEngineConfiguration config = (SpringProcessEngineConfiguration) bean;
                  
                  // Database
                  config.setJdbcMaxActiveConnections(activitiDataSourceProperties.getJdbcMaxActiveConnections());
                  config.setJdbcMaxIdleConnections(activitiDataSourceProperties.getJdbcMaxIdleConnections());
                  config.setJdbcMaxCheckoutTime(activitiDataSourceProperties.getJdbcMaxCheckoutTime());
                  config.setJdbcMaxWaitTime(activitiDataSourceProperties.getJdbcMaxWaitTime());
                  config.setEnableDatabaseEventLogging(activitiDataSourceProperties.getDbEnableEventLogging());
                  
                  // Async Job Executor
                  DefaultAsyncJobExecutor asyncExecutor = new DefaultAsyncJobExecutor();
                  asyncExecutor.setAsyncJobLockTimeInMillis(activitiAsycExecutorProperties.getAsyncJobLockTimeInMillis());
                  asyncExecutor.setCorePoolSize(activitiAsycExecutorProperties.getCorePoolSize());
                  asyncExecutor.setDefaultAsyncJobAcquireWaitTimeInMillis(activitiAsycExecutorProperties.getDefaultAsyncJobAcquireWaitTimeInMillis());
                  asyncExecutor.setDefaultTimerJobAcquireWaitTimeInMillis(activitiAsycExecutorProperties.getDefaultTimerJobAcquireWaitTimeInMillis());
                  asyncExecutor.setKeepAliveTime(activitiAsycExecutorProperties.getKeepAliveTime());
                  asyncExecutor.setMaxAsyncJobsDuePerAcquisition(activitiAsycExecutorProperties.getMaxAsyncJobsDuePerAcquisition());
                  asyncExecutor.setMaxPoolSize(activitiAsycExecutorProperties.getMaxPoolSize());
                  asyncExecutor.setMaxTimerJobsPerAcquisition(activitiAsycExecutorProperties.getMaxTimerJobsPerAcquisition());
                  asyncExecutor.setQueueSize(activitiAsycExecutorProperties.getQueueSize());
                  asyncExecutor.setTimerLockTimeInMillis(activitiAsycExecutorProperties.getTimerLockTimeInMillis());
                  config.setAsyncExecutor(asyncExecutor);
            }
            return bean;
        }

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }           
    };
}
...
...

Finally, open the application.properties file and add the following properties:

  • spring.activiti.async-executor-activate: Instructs the Activiti Engine to startup the Async Executor thread pool at startup. Can be true (activate) of false (suspend)
  • spring.activiti.async-executor-enabled: Enables the Async executor instead of the old Job executor. Can be true (enable) of false (disable)
  • spring.activiti.check-process-definitions: Whether to automatically deploy resources. Can be true (deploy) of false (not deploy)
  • spring.activiti.history-level: Following history levels can be configured:
    • none: skips all history archiving. This is the most performant for runtime process execution, but no historical information will be available
    • activity: archives all process instances and activity instances. At the end of the process instance, the latest values of the top level process instance variables will be copied to historic variable instances. No details will be archived
    • audit: This is the default. It archives all process instances, activity instances, keeps variable values continuously in sync and all form properties that are submitted so that all user interaction through forms is traceable and can be audited
    • full: This is the highest level of history archiving and hence the slowest. This level stores all information as in the audit level plus all other possible details, mostly this are process variable updates
  • spring.activiti.job-executor-activate: Instructs the Activiti Engine to startup the Job Executor. Can be true (activate) of false (suspend)

By setting the property spring.activiti.check-process-definitions to false, we can now change back to Activiti’s version 5.22 without getting an error when running our application, as we have instructed Activiti not to deploy the resources automatically.

Summary

In this post, you will learned:

  • How to integrate Activiti’s engine and REST API into your Spring application.
  • Use Activiti with an in-memory H2 database and/or with other databases.
  • Create configuration property classes, which were used for modifying Activiti’s data source and Async Job Executor behavior.
  • Configured Activiti’s process engine to fit your needs.

Hope you enjoyed this post as much as I did writing it. Please leave your comments and feedback.

About canchitodev

Professional with solid experience in software development and management, leadership, team-building, workflow design and technological development. I consider myself a proactive, creative problem-solver, results-driven and with exceptional interpersonal skills person, who likes challenges, working as part of a team, and in high demanding environments. In these last few years, my career has focused on software management, development and design in the Media Broadcasting for television stations, thus allowing the automation of workflows

0 0 votes
Article Rating
Subscribe
Notify of

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
trackback
4 years ago

[…] Integrate Activiti with Spring […]

trackback
2 years ago

[…] Integrate Activiti with Spring […]