Update Spring Boot & Hateoas (#430)
* Upgrade spring boot 1.4.4 and hateoas 0.23. Removed unneded dependency, Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Avoid link change with new hateoas version. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Readded commons.io Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Update MariaDB driver to 1.5.7 Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Added missing content to docs. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Fix equals. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Simplify Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Fix equal after removal of commons collections. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
href: "/documentation/architecture/datamodel.html"
|
||||
- title: "Target States"
|
||||
href: "/documentation/architecture/targetstate.html"
|
||||
- title: "Rollout Management"
|
||||
href: "/documentation/architecture/rollout-management.html"
|
||||
|
||||
- title: "Interfaces"
|
||||
href: ""
|
||||
@@ -48,4 +50,4 @@
|
||||
- title: "Theme Customization"
|
||||
href: "/documentation/guide/customtheme.html"
|
||||
- title: "Create Feign Client"
|
||||
href: "/documentation/guide/feignclient.html"
|
||||
href: "/documentation/guide/feignclient.html"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
layout: documentation
|
||||
title: Rollout Management
|
||||
---
|
||||
|
||||
{% include base.html %}
|
||||
|
||||
Software update operations in large scale IoT scenarios with hundreds of thousands of devices require special handling.
|
||||
|
||||
That includes:
|
||||
- _Technical Scalability_ by means of horizontal scale of the _Rollouts_ server cluster in the cloud.
|
||||
- _Global_ artifact _content delivery_ capacities.
|
||||
- _Functional Scalability_ by means of:
|
||||
- Secure handling of large volumes of devices at rollout creation time.
|
||||
- Monitoring of the rollout progress.
|
||||
- Emergency rollout shutdown in case of problems on to many devices.
|
||||
|
||||
- Reporting capabilities for a complete understanding of the rollout progress at each point in time.
|
||||
|
||||
Eclipse _hawkBit_ sees these capabilities under the term Rollout Management.
|
||||
|
||||
The following capabilities are currently supported by the _Rollout Management_:
|
||||
- Create, update and start of rollouts.
|
||||
- Selection of targets as input for the rollout based on _target filter_ functionality.
|
||||
- Selection of a _DistributionSet_.
|
||||
- Auto-splitting of the input target list into a defined number deployment groups.
|
||||
|
||||
- Cascading start of the deployment groups based on installation status of the previous group.
|
||||
- Emergency shutdown of the rollout in case a group exceeds the defined error threshold.
|
||||
- Rollout progress monitoring for the entire rollout and the individual groups.
|
||||
|
||||
|
||||
## Cascading Deployment Group Execution
|
||||
The cascading execution of the deployment groups is based on two thresholds that can be defined by the rollout creator.
|
||||
- success condition by means of percentage of successfully installed targets in the current groups triggers.
|
||||
- error condition by means of absolute or percentage of failed installations which triggers an emergency shutdown of the entire rollout.
|
||||
|
||||
[[images/DeploymentGroups.png]]
|
||||
|
||||
## Rollout state machine
|
||||
### State Machine on Rollout
|
||||
{:width="100%" .image-center}
|
||||
|
||||
### State Machine on Rollout Deployment Group
|
||||
{:width="100%" .image-center}
|
||||
@@ -9,7 +9,7 @@ title: Clustering
|
||||
|
||||
_hawkBit_ is able to run in a cluster with some constraints. This guide provides insights in the basic concepts and how to setup your own cluster. You can find additional information in the [hawkbit example app's README](https://github.com/eclipse/hawkbit/blob/master/examples/hawkbit-example-app/README.md).
|
||||
|
||||
# Big picture
|
||||
# Big picture
|
||||
|
||||
{:width="100%"}
|
||||
|
||||
@@ -18,7 +18,7 @@ _hawkBit_ is able to run in a cluster with some constraints. This guide provides
|
||||
Event communication between nodes is based on [Spring Cloud Bus](https://cloud.spring.io/spring-cloud-bus/) and [Spring Cloud Stream](http://docs.spring.io/spring-cloud-stream/docs/current/reference/htmlsingle/). There are different [binder implementations](http://docs.spring.io/spring-cloud-stream/docs/current/reference/htmlsingle/#_binders) available. The _hawkbit example app_ uses RabbitMQ binder. Every node gets his own queue to receive cluster events, the default payload is JSON.
|
||||
If an event is thrown locally at one node, it will be automatically delivered to all other available nodes via the Spring Cloud Bus's topic exchange:
|
||||
|
||||
{:width="100%"}
|
||||
[[images/eventing-within-cluster.png]]
|
||||
|
||||
Via the ServiceMatcher you can check whether an event happened locally at one node or on a different node.
|
||||
`serviceMatcher.isFromSelf(event)`
|
||||
@@ -26,7 +26,7 @@ Via the ServiceMatcher you can check whether an event happened locally at one no
|
||||
# Caching
|
||||
|
||||
Every node is maintaining its own caches independent from other nodes. So there is no globally shared/synchronized cache instance within the cluster. In order to keep nodes in sync a TTL (time to live) can be set for all caches to ensure that after some time the cache is refreshed from the database. To enable the TTL just set the property "hawkbit.cache.global.ttl" (value in milliseconds).
|
||||
Of course you can implement a shared cache, e.g. Redis.
|
||||
Of course you can implement a shared cache, e.g. Redis.
|
||||
See [CacheAutoConfiguration](https://github.com/eclipse/hawkbit/blob/master/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/CacheAutoConfiguration.java)
|
||||
|
||||
# Schedulers
|
||||
@@ -36,7 +36,7 @@ Every node has multiple schedulers which run after a defined period of time. All
|
||||
# Known constraints
|
||||
|
||||
## UI sessions
|
||||
As of today _hawkBit_ isn't storing user sessions in a shared, clusterwide cache. Session is only bound to the node where the login took place. If this node is going down for whatever reason, the session is lost and the user is forced to login again.
|
||||
As of today _hawkBit_ isn't storing user sessions in a shared, clusterwide cache. Session is only bound to the node where the login took place. If this node is going down for whatever reason, the session is lost and the user is forced to login again.
|
||||
In case that's not an option, you can help yourself by introducing a shared session cache based on e.g. Redis.
|
||||
Furthermore _hawkBit_ isn't supporting session stickiness out of the box either. However most of the well known load balancers out there can solve this issue.
|
||||
|
||||
@@ -49,4 +49,3 @@ In a cluster-capable environment this fact can lead to issues as it could happen
|
||||
_hawkbit_ owns the feature of guarding itself from DoS attacks, a [DoS filter](https://github.com/eclipse/hawkbit/blob/master/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/DosFilter.java). It reduces the maximum number of requests per seconds which can be configured for read and write requests.
|
||||
This mechanism is only working for every node separately, i.e. in a cluster environment the worst-case behaviour would be that the maximum number of requests per seconds will be increased to its product if every request is handled by a different node.
|
||||
The same constraint exists with the validator to check if a user tried too many logins within a defined period of time.
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -36,7 +36,7 @@ Available Management APIs resources are:
|
||||
* [Software Module Types](https://docs.bosch-iot-rollouts.com/documentation/rest-api/softwaremoduletypes-api-guide.html)
|
||||
* [Target Tag](https://docs.bosch-iot-rollouts.com/documentation/rest-api/targettag-api-guide.html)
|
||||
* [Distribution Set Tag](https://docs.bosch-iot-rollouts.com/documentation/rest-api/distributionsettag-api-guide.html)
|
||||
* [Rollouts](http://https://docs.bosch-iot-rollouts.com/documentation/developerguide/apispecifications/managementapi/rollouts.html)
|
||||
* [Rollouts](https://docs.bosch-iot-rollouts.com/documentation/rest-api/rollout-api-guide.html)
|
||||
|
||||
|
||||
## Headers
|
||||
@@ -74,4 +74,4 @@ A _Distribution Set_ entity may have for example URIs to artifacts, _Software Mo
|
||||
"metadata": {
|
||||
"href": "http://localhost:8080/rest/v1/softwaremodules/83/metadata?offset=0&limit=50"
|
||||
}
|
||||
{% endhighlight %}
|
||||
{% endhighlight %}
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.artifact.MgmtArtifact;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.artifact.MgmtArtifactHash;
|
||||
@@ -116,7 +117,7 @@ public final class MgmtSoftwareModuleMapper {
|
||||
response.add(linkTo(methodOn(MgmtSoftwareModuleResource.class).getMetadata(response.getModuleId(),
|
||||
Integer.parseInt(MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET),
|
||||
Integer.parseInt(MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT), null, null))
|
||||
.withRel("metadata"));
|
||||
.withRel("metadata").expand(ArrayUtils.toArray()));
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.MgmtPollStatus;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus;
|
||||
@@ -65,7 +66,7 @@ public final class MgmtTargetMapper {
|
||||
response.add(linkTo(methodOn(MgmtTargetRestApi.class).getActionHistory(response.getControllerId(), 0,
|
||||
MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE,
|
||||
ActionFields.ID.getFieldName() + ":" + SortDirection.DESC, null))
|
||||
.withRel(MgmtRestConstants.TARGET_V1_ACTIONS));
|
||||
.withRel(MgmtRestConstants.TARGET_V1_ACTIONS).expand(ArrayUtils.toArray()));
|
||||
}
|
||||
|
||||
static void addPollStatus(final Target target, final MgmtTarget targetRest) {
|
||||
|
||||
@@ -81,10 +81,6 @@
|
||||
<groupId>cz.jirutka.rsql</groupId>
|
||||
<artifactId>rsql-parser</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
|
||||
@@ -18,7 +18,6 @@ import java.util.stream.Collectors;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetFields;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetMetadataFields;
|
||||
@@ -69,6 +68,7 @@ import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.transaction.annotation.Isolation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
@@ -12,7 +12,6 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
||||
import org.eclipse.hawkbit.repository.SoftwareManagement;
|
||||
import org.eclipse.hawkbit.repository.builder.AbstractDistributionSetUpdateCreate;
|
||||
@@ -21,6 +20,7 @@ import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Create/build implementation.
|
||||
|
||||
@@ -11,13 +11,13 @@ package org.eclipse.hawkbit.repository.jpa.builder;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.eclipse.hawkbit.repository.SoftwareManagement;
|
||||
import org.eclipse.hawkbit.repository.builder.AbstractDistributionSetTypeUpdateCreate;
|
||||
import org.eclipse.hawkbit.repository.builder.DistributionSetTypeCreate;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Create/build implementation.
|
||||
|
||||
@@ -25,12 +25,12 @@ import javax.persistence.Table;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A distribution set type defines which software module types can or have to be
|
||||
|
||||
@@ -204,10 +204,6 @@
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
|
||||
@@ -19,7 +19,6 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.hawkbit.ui.artifacts.smtable.SoftwareModuleAddUpdateWindow;
|
||||
import org.eclipse.hawkbit.ui.components.SPUIComponentProvider;
|
||||
@@ -33,7 +32,6 @@ import org.vaadin.hene.flexibleoptiongroup.FlexibleOptionGroupItemComponent;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.vaadin.data.Container.ItemSetChangeEvent;
|
||||
import com.vaadin.data.Container.ItemSetChangeListener;
|
||||
import com.vaadin.data.Property.ValueChangeEvent;
|
||||
@@ -240,7 +238,7 @@ public class CommonDialogWindow extends Window {
|
||||
Object value = field.getValue();
|
||||
|
||||
if (field instanceof Table) {
|
||||
value = Sets.newHashSet(((Table) field).getContainerDataSource().getItemIds());
|
||||
value = ((Table) field).getContainerDataSource().getItemIds();
|
||||
}
|
||||
orginalValues.put(field, value);
|
||||
}
|
||||
@@ -263,6 +261,9 @@ public class CommonDialogWindow extends Window {
|
||||
}
|
||||
|
||||
protected void addComponentListeners() {
|
||||
// avoid duplicate registration
|
||||
removeListeners();
|
||||
|
||||
for (final AbstractField<?> field : allComponents) {
|
||||
if (field instanceof TextChangeNotifier) {
|
||||
((TextChangeNotifier) field).addTextChangeListener(new ChangeListener(field));
|
||||
@@ -290,28 +291,13 @@ public class CommonDialogWindow extends Window {
|
||||
}
|
||||
final Object currentValue = getCurrentVaue(currentChangedComponent, newValue, field);
|
||||
|
||||
if (!isValueEquals(field, originalValue, currentValue)) {
|
||||
if (!Objects.equals(originalValue, currentValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isValueEquals(final AbstractField<?> field, final Object orginalValue,
|
||||
final Object currentValue) {
|
||||
if (Set.class.equals(field.getType())) {
|
||||
return CollectionUtils.isEqualCollection(CollectionUtils.emptyIfNull((Collection<?>) orginalValue),
|
||||
CollectionUtils.emptyIfNull((Collection<?>) currentValue));
|
||||
}
|
||||
|
||||
if (String.class.equals(field.getType())) {
|
||||
return Objects.equals(Strings.emptyToNull((String) orginalValue),
|
||||
Strings.emptyToNull((String) currentValue));
|
||||
}
|
||||
|
||||
return Objects.equals(orginalValue, currentValue);
|
||||
}
|
||||
|
||||
private static Object getCurrentVaue(final Component currentChangedComponent, final Object newValue,
|
||||
final AbstractField<?> field) {
|
||||
Object currentValue = field.getValue();
|
||||
|
||||
@@ -20,7 +20,6 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.eclipse.hawkbit.repository.FilterParams;
|
||||
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
|
||||
import org.eclipse.hawkbit.repository.TargetManagement;
|
||||
@@ -39,6 +38,7 @@ import org.eclipse.hawkbit.ui.utils.SpringContextHelper;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.vaadin.addons.lazyquerycontainer.AbstractBeanQuery;
|
||||
import org.vaadin.addons.lazyquerycontainer.QueryDefinition;
|
||||
|
||||
@@ -211,7 +211,7 @@ public class TargetBeanQuery extends AbstractBeanQuery<ProxyTarget> {
|
||||
|
||||
private boolean isAnyFilterSelected() {
|
||||
final boolean isFilterSelected = isTagSelected() || isOverdueFilterEnabled();
|
||||
return isFilterSelected || CollectionUtils.isNotEmpty(status) || distributionId != null
|
||||
return isFilterSelected || !CollectionUtils.isEmpty(status) || distributionId != null
|
||||
|| !isNullOrEmpty(searchText);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
||||
import org.eclipse.hawkbit.repository.FilterParams;
|
||||
import org.eclipse.hawkbit.repository.TagManagement;
|
||||
@@ -70,6 +69,7 @@ import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider;
|
||||
import org.eclipse.hawkbit.ui.utils.UINotification;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.vaadin.addons.lazyquerycontainer.BeanQueryFactory;
|
||||
import org.vaadin.addons.lazyquerycontainer.LazyQueryContainer;
|
||||
import org.vaadin.addons.lazyquerycontainer.LazyQueryDefinition;
|
||||
|
||||
13
pom.xml
13
pom.xml
@@ -14,7 +14,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>1.4.3.RELEASE</version>
|
||||
<version>1.4.4.RELEASE</version>
|
||||
</parent>
|
||||
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
@@ -101,11 +101,12 @@
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<spring.boot.version>1.4.3.RELEASE</spring.boot.version>
|
||||
<spring.boot.version>1.4.4.RELEASE</spring.boot.version>
|
||||
<snapshotDependencyAllowed>true</snapshotDependencyAllowed>
|
||||
|
||||
<!-- Spring boot version overrides (should be reviewed with every boot upgrade) - START -->
|
||||
<!-- Newer versions needed than defined in Boot -->
|
||||
<spring-hateoas.version>0.23.0.RELEASE</spring-hateoas.version>
|
||||
<!-- Older versions needed than defined in Boot -->
|
||||
<!-- Incompatible changes in 2.2 -->
|
||||
<json-path.version>2.0.0</json-path.version>
|
||||
@@ -137,14 +138,13 @@
|
||||
<gwtmockito.version>1.1.6</gwtmockito.version>
|
||||
<pl.pragmatists.version>1.0.2</pl.pragmatists.version>
|
||||
<guava.version>19.0</guava.version>
|
||||
<mariadb-java-client.version>1.5.3</mariadb-java-client.version>
|
||||
<mariadb-java-client.version>1.5.7</mariadb-java-client.version>
|
||||
<embedded-mongo.version>1.50.5</embedded-mongo.version>
|
||||
<javax.el-api.version>2.2.4</javax.el-api.version>
|
||||
<corn-cps.version>1.1.7</corn-cps.version>
|
||||
<jlorem.version>1.1</jlorem.version>
|
||||
<json-simple.version>1.1.1</json-simple.version>
|
||||
<commons-lang3.version>3.4</commons-lang3.version>
|
||||
<commons-collections4.version>4.1</commons-collections4.version>
|
||||
<json.version>20141113</json.version>
|
||||
<rsql-parser.version>2.1.0</rsql-parser.version>
|
||||
<spring.cloud.version>Camden.SR1</spring.cloud.version>
|
||||
@@ -668,11 +668,6 @@
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>${commons-lang3.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
<version>${commons-collections4.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
Reference in New Issue
Block a user