Commit 50016c9e authored by Matthias Betz's avatar Matthias Betz
Browse files

add geolocation extraction service

parent 598378fc
......@@ -12,10 +12,14 @@ It can be used behind a reverse-proxy like apache or nginx.
To configure the conversion binary use the start parameter `-Dconverter.executable=./IfcConvert`. The converter binaries for linux and
windows are included in this repository. The service requires executable rights for the converter program.
In addition to extract geolocation information from the IFC files a python script is used. The python script requires the package `ifcopenshell`
to be installed via `pip install ifcopenshell`. Python has to be available on the path, the service searches for python as well as python3 executable.
## Packaging
To create a runnable package. This does not package the converter program into the jar. It has to be deployed seperatly.
Alternatively a Dockerfile has been provided
```
mvn package
......
......@@ -13,6 +13,14 @@ COPY IfcConvert /app/IfcConvert
# Ensure it's executable
RUN chmod +x /app/IfcConvert
# Install Python and pip
RUN apt-get update && \
apt-get install -y python3 python3-pip && \
rm -rf /var/lib/apt/lists/*
# Install ifcopenshell Python package
RUN pip3 install ifcopenshell
# Expose the port your Spring Boot app runs on
EXPOSE 80
......
import ifcopenshell
import sys
import json
import math
def extract_geolocation(ifc_file):
model = ifcopenshell.open(ifc_file)
# data structure for geolocation
data = []
# --- IfcSite ---
sites = model.by_type("IfcSite")
for site in sites:
georef = {"type": "LoGeoRef20"}
if site.RefLatitude and site.RefLongitude:
lat = compound_angle_to_float(site.RefLatitude)
lon = compound_angle_to_float(site.RefLongitude)
georef["lat"] = lat
georef["lon"] = lon
data.append(georef)
if site.RefElevation is not None:
georef["ele"] = site.RefElevation
# --- IfcSite.ObjectPlacement ---
if site.ObjectPlacement is not None:
if site.ObjectPlacement.is_a('IfcGridPlacement'):
pass
elif site.ObjectPlacement.is_a('IfcLinearPlacement'):
pass
elif site.ObjectPlacement.is_a('IfcLocalPlacement'):
relativePlacement = site.ObjectPlacement.RelativePlacement
if relativePlacement.is_a('IfcAxis2Placement3D'):
if (relativePlacement.Location is not None):
georef = {"type": "LoGeoRef30"}
coords = list(relativePlacement.Location.Coordinates)
if all(v == 0 for v in coords):
continue
georef["lon"] = coords[0]
georef["lat"] = coords[1]
georef["ele"] = coords[2] if len(coords) > 2 else 0
data.append(georef)
elif relativePlacement.is_a('IfcAxis2Placement2D'):
georef = {"type": "LoGeoRef30"}
coords = list(relativePlacement.Location.Coordinates)
if all(v == 0 for v in coords):
continue
georef["lon"] = coords[0]
georef["lat"] = coords[1]
georef["ele"] = coords[2] if len(coords) > 2 else 0
data.append(georef)
# --- IfcBuilding ---
sites = model.by_type("IfcBuilding")
for site in sites:
# --- IfcBuilding.ObjectPlacement ---
if site.ObjectPlacement is not None:
if site.ObjectPlacement.is_a('IfcGridPlacement'):
pass
elif site.ObjectPlacement.is_a('IfcLinearPlacement'):
pass
elif site.ObjectPlacement.is_a('IfcLocalPlacement'):
relativePlacement = site.ObjectPlacement.RelativePlacement
if relativePlacement.is_a('IfcAxis2Placement3D'):
if (relativePlacement.Location is not None):
georef = {"type": "LoGeoRef30"}
coords = list(relativePlacement.Location.Coordinates)
if all(v == 0 for v in coords):
continue
georef["lon"] = coords[0]
georef["lat"] = coords[1]
georef["ele"] = coords[2] if len(coords) > 2 else 0
data.append(georef)
elif relativePlacement.is_a('IfcAxis2Placement2D'):
georef = {"type": "LoGeoRef30"}
coords = list(relativePlacement.Location.Coordinates)
if all(v == 0 for v in coords):
continue
georef["lon"] = coords[0]
georef["lat"] = coords[1]
georef["ele"] = coords[2] if len(coords) > 2 else 0
data.append(georef)
# --- IFCPROJECT ---
project = model.by_type("IfcProject")
for p in project:
repContext = p.RepresentationContexts
for rep in repContext:
if rep.is_a("IfcGeometricRepresentationContext"):
crs = rep.WorldCoordinateSystem
if crs is None:
continue
georef = {"type": "LoGeoRef40"}
if crs.is_a('IfcAxis2Placement3D'):
if (crs.Location is not None):
coords = list(crs.Location.Coordinates)
if all(v == 0 for v in coords):
continue
georef["lon"] = coords[0]
georef["lat"] = coords[1]
georef["ele"] = coords[2] if len(coords) > 2 else 0
data.append(georef)
elif crs.is_a('IfcAxis2Placement2D'):
coords = list(crs.Location.Coordinates)
if all(v == 0 for v in coords):
continue
georef["lon"] = coords[0]
georef["lat"] = coords[1]
georef["ele"] = coords[2] if len(coords) > 2 else 0
data.append(georef)
north = rep.TrueNorth
if north is not None:
vec = normalize(list(north.DirectionRatios))
georef["axisX"] = vec[0]
georef["axisY"] = vec[1]
# --- IfcMapConversion ---
try:
conversions = model.by_type("IfcMapConversion")
for conv in conversions:
georef = {"type": "LoGeoRef50"}
if conv.SourceCRS.is_a('IfcGeometricRepresentationContext'):
# local coordinate system
pass
else:
pass
georef["targetCRS"] = conv.TargetCRS.Name
georef["northings"] = conv.Northings
georef["eastings"] = conv.Eastings
georef["ele"] = conv.OrthogonalHeight
if conv.XAxisAbscissa is not None:
georef["XAxisAbscissa"] = conv.XAxisAbscissa
if conv.XAxisOrdinate is not None:
georef["XAxisOrdinate"] = conv.XAxisOrdinate
data.append(georef)
except RuntimeError:
pass
return data
def normalize(vec):
length = math.sqrt(sum(c**2 for c in vec))
return [c / length for c in vec] if length != 0 else vec
def compound_angle_to_float(angle_list):
if not angle_list or len(angle_list) not in (3, 4):
raise ValueError("Input must be a list of 3 or 4 integers: [d, m, s, (ms)]")
d, m, s = angle_list[:3]
ms = angle_list[3] if len(angle_list) == 4 else 0
return d + m / 60.0 + s / 3600.0 + ms / 3_600_000_000.0
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python myscript.py <ifcFile>")
sys.exit(1)
print(json.dumps(extract_geolocation(sys.argv[1])))
......@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>de.hft.stuttgart</groupId>
<artifactId>ifc-converter-server</artifactId>
<version>1.1.0</version>
<version>1.1.1</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
......@@ -28,6 +28,35 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.locationtech.proj4j</groupId>
<artifactId>proj4j</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.locationtech.proj4j</groupId>
<artifactId>proj4j-epsg</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
<build>
......
......@@ -10,9 +10,10 @@ import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -32,19 +33,21 @@ import jakarta.servlet.http.HttpServletRequest;
@CrossOrigin
@RestController
public class ConverterEndpoints {
@Value("${converter.executable}")
private String converterExecutable;
private GeolocationExtractionService geolocationService;
@Autowired
public ConverterEndpoints() {
// constructor for injection
public ConverterEndpoints(GeolocationExtractionService geolocationService) {
this.geolocationService = geolocationService;
}
private static final Logger logger = LoggerFactory.getLogger(ConverterEndpoints.class);
@PostMapping(path = "/convert",produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<StreamingResponseBody> checkAddress(@RequestParam("file") MultipartFile file,
@PostMapping(path = "/convert", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<StreamingResponseBody> convertIfc(@RequestParam("file") MultipartFile file,
HttpServletRequest request) {
logger.info("Convert request: {}", file.getOriginalFilename());
......@@ -55,6 +58,9 @@ public class ConverterEndpoints {
try {
Files.createDirectories(workFolder);
Files.copy(file.getInputStream(), ifcFile);
CompletableFuture<String> geolocation = checkGeolocation(ifcFile);
List<String> commands = new ArrayList<>();
commands.add(converterExecutable);
commands.add(ifcFile.toAbsolutePath().toString());
......@@ -75,33 +81,67 @@ public class ConverterEndpoints {
process.waitFor(10, TimeUnit.MINUTES);
int exitValue = process.exitValue();
logger.info("Converter exit code for file {} is {}", file.getOriginalFilename(), exitValue);
Files.delete(ifcFile);
String originalFileName = file.getOriginalFilename();
if (originalFileName == null) {
originalFileName = "converted.ifc";
}
String outputFileName = originalFileName.substring(0, originalFileName.lastIndexOf('.')) + ".glb";
return ResponseEntity.ok().contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + outputFileName + "\"")
.body(new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
try {
Files.copy(output, outputStream);
} finally {
Files.delete(output);
String outputFileName = output.getFileName().toString();
String geolocationString = extractGeolocationString(geolocation);
if (geolocationString == null) {
return ResponseEntity.ok().contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + outputFileName + "\"")
.body(new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
try {
Files.copy(output, outputStream);
} finally {
Files.delete(output);
}
}
}
});
} catch (IOException | InterruptedException e) {
});
} else {
return ResponseEntity.ok().contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + outputFileName + "\"")
.header("X-geolocation", geolocationString)
.header("Access-Control-Expose-Headers", "X-geolocation, Content-Disposition")
.body(new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
try {
Files.copy(output, outputStream);
} finally {
Files.delete(output);
}
}
});
}
} catch (IOException | InterruptedException | ExecutionException e) {
logger.error("Failed to convert file: {}", file.getOriginalFilename(), e);
return ResponseEntity.internalServerError().build();
} finally {
try {
Files.delete(output);
Files.delete(ifcFile);
} catch (IOException ex) {
}
return ResponseEntity.internalServerError().build();
}
}
private String extractGeolocationString(CompletableFuture<String> geolocation) throws InterruptedException, ExecutionException {
try {
return geolocation.get();
} catch (Exception e) {
logger.info("Failed to extract geolocation", e);
return null;
}
}
private CompletableFuture<String> checkGeolocation(Path ifcFile) {
return geolocationService.extractGeolocation(ifcFile);
}
}
package de.hft.stuttgart.lgl.ifc.controller;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ProcessBuilder.Redirect;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import org.locationtech.proj4j.BasicCoordinateTransform;
import org.locationtech.proj4j.CRSFactory;
import org.locationtech.proj4j.CoordinateReferenceSystem;
import org.locationtech.proj4j.CoordinateTransform;
import org.locationtech.proj4j.ProjCoordinate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.hft.stuttgart.lgl.ifc.model.Geolocation;
@Service
public class GeolocationExtractionService {
private static final Logger logger = LoggerFactory.getLogger(GeolocationExtractionService.class);
private static final ObjectMapper mapper = new ObjectMapper();
private CRSFactory crsFactory = new CRSFactory();
private CoordinateReferenceSystem gsk3 = crsFactory.createFromName("EPSG:31467");
private CoordinateReferenceSystem utm32 = crsFactory.createFromName("EPSG:25832");
private CoordinateReferenceSystem wgs84 = crsFactory.createFromName("EPSG:4326");
private CoordinateTransform utm32ToWgs84Transform = new BasicCoordinateTransform(utm32, wgs84);
private CoordinateTransform gsk3ToWgs84Transform = new BasicCoordinateTransform(gsk3, wgs84);
private String pythonExecutable = null;
@Async
public CompletableFuture<String> extractGeolocation(Path ifcFile) {
return CompletableFuture.supplyAsync(() -> {
if (pythonExecutable == null) {
pythonExecutable = getAvailablePython();
}
List<String> commands = new ArrayList<>();
commands.add(pythonExecutable);
commands.add("extract_geolocation.py");
commands.add(ifcFile.toString());
ProcessBuilder processBuilder = new ProcessBuilder(commands);
processBuilder.redirectError(Redirect.INHERIT);
try {
Process process = processBuilder.start();
process.onExit().thenApply(p -> {
logger.info("Geolocation extraction process exited with: {}", process.exitValue());
return 0;
});
byte[] allBytes = process.getInputStream().readAllBytes();
if (allBytes.length == 0) {
return null;
}
List<Geolocation> data = mapper.readValue(allBytes, new TypeReference<>() {
});
for (Geolocation loc : data) {
logger.info("Found geolocation: {}", loc);
}
return findMostViableLocation(data);
} catch (Exception e) {
throw new CompletionException(e);
}
});
}
public static String getAvailablePython() {
if (isCommandAvailable("python")) {
return "python";
} else if (isCommandAvailable("python3")) {
return "python3";
} else {
throw new IllegalArgumentException(
"Could not find either python or python3 executable for python installation");
}
}
private static boolean isCommandAvailable(String command) {
try {
ProcessBuilder pb = new ProcessBuilder(command, "--version");
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line = reader.readLine();
if (line != null && line.toLowerCase().contains("python")) {
return true;
}
}
int exitCode = process.waitFor();
return exitCode == 0;
} catch (IOException | InterruptedException e) {
return false;
}
}
private String findMostViableLocation(List<Geolocation> data) {
if (data.isEmpty()) {
return null;
}
String geoRef50 = extractViableGeoRef50Data(data);
if (geoRef50 != null) {
logger.info("Using LoGeoRef50: {}", geoRef50);
return geoRef50;
}
String geoRef40 = extractViableGeoRef40Data(data);
if (geoRef40 != null) {
logger.info("Using LoGeoRef40: {}", geoRef40);
return geoRef40;
}
String geoRef30 = extractViableGeoRef30Data(data);
if (geoRef30 != null) {
logger.info("Using LoGeoRef30: {}", geoRef30);
return geoRef30;
}
String geoRef20 = extractViableGeoRef20Data(data);
if (geoRef20 != null) {
logger.info("Using LoGeoRef20: {}", geoRef20);
}
return geoRef20;
}
private String extractViableGeoRef50Data(List<Geolocation> data) {
try {
Geolocation loc = findByType(data, "LoGeoRef50");
if (loc == null) {
return null;
}
String epsg = (String) loc.getReferenceData().get("targetCRS");
double x = (double) loc.getReferenceData().get("eastings");
double y = (double) loc.getReferenceData().get("northings");
Double elevation = (Double) loc.getReferenceData().get("ele");
double[] lonLat = convertToWgs84(x, y, epsg);
if (lonLat == null) {
return null;
}
Double abscissa = (Double) loc.getReferenceData().get("XAxisAbscissa");
Double ordinate = (Double) loc.getReferenceData().get("XAxisOrdinate");
if (abscissa != null && ordinate != null) {
// found north axis as well
double heading = Math.atan2(ordinate, abscissa);
return createLongLatElevationWithHeadingString(lonLat, elevation, heading);
}
return createLongLatElevationString(lonLat, elevation);
} catch (Exception e) {
logger.info("Failed to read LoGeoRef50: {}, {}", e.getClass().getSimpleName(), e.getMessage());
return null;
}
}
private double[] convertToWgs84(double x, double y, String epsg) {
CoordinateReferenceSystem crs = crsFactory.createFromName(epsg);
BasicCoordinateTransform bct = new BasicCoordinateTransform(crs, wgs84);
logger.info("Found {}", epsg);
ProjCoordinate c1 = new ProjCoordinate(x, y);
ProjCoordinate c2 = new ProjCoordinate();
bct.transform(c1, c2);
return new double[] { c2.x, c2.y };
}
private String extractViableGeoRef40Data(List<Geolocation> data) {
try {
Geolocation loc = findByType(data, "LoGeoRef40");
if (loc == null) {
return null;
}
double lon = (double) loc.getReferenceData().get("lon");
double lat = (double) loc.getReferenceData().get("lat");
Double elevation = (Double) loc.getReferenceData().get("ele");
double[] lonLat = convertToWgs84(lon, lat);
if (lonLat == null) {
return null;
}
Double axisX = (Double) loc.getReferenceData().get("axisX");
Double axisY = (Double) loc.getReferenceData().get("axisY");
if (axisX != null && axisY != null) {
// found north axis as well
double heading = Math.atan2(axisY, axisX);
return createLongLatElevationWithHeadingString(lonLat, elevation, heading);
}
return createLongLatElevationString(lonLat, elevation);
} catch (Exception e) {
return null;
}
}
private String createLongLatElevationWithHeadingString(double[] lonLat, Double elevation, double heading) {
if (elevation == null || (elevation < 0.001 && elevation > -0.001)) {
return String.format(Locale.US, "{\"lon\":%.15f,\"lat\":%.15f, \"heading\": %.15f}", lonLat[0], lonLat[1], heading);
} else {
return String.format(Locale.US, "{\"lon\": %.15f, \"lat\": %.15f, \"heading\": %.15f, \"ele\": %.15f}", lonLat[0],
lonLat[1], heading, elevation);
}
}
private String extractViableGeoRef30Data(List<Geolocation> data) {
try {
Geolocation loc = findByType(data, "LoGeoRef30");
if (loc == null) {
return null;
}
double lon = (double) loc.getReferenceData().get("lon");
double lat = (double) loc.getReferenceData().get("lat");
Double elevation = (Double) loc.getReferenceData().get("ele");
double[] lonLat = convertToWgs84(lon, lat);
if (lonLat == null) {
return null;
}
return createLongLatElevationString(lonLat, elevation);
} catch (Exception e) {
return null;
}
}
private String createLongLatElevationString(double[] lonLat, Double elevation) {
if (elevation == null || (elevation < 0.001 && elevation > -0.001)) {
return String.format(Locale.US, "{\"lon\":%.15f,\"lat\":%.15f}", lonLat[0], lonLat[1]);
} else {
return String.format(Locale.US, "{\"lon\": %.15f, \"lat\": %.15f, \"ele\": %.15f}", lonLat[0], lonLat[1], elevation);
}
}
private String extractViableGeoRef20Data(List<Geolocation> data) {
try {
Geolocation loc = findByType(data, "LoGeoRef20");
if (loc == null) {
return null;
}
double lon = (double) loc.getReferenceData().get("lon");
double lat = (double) loc.getReferenceData().get("lat");
Double elevation = (Double) loc.getReferenceData().get("ele");
double[] lonLat = convertToWgs84(lon, lat);
if (lonLat == null) {
return null;
}
return createLongLatElevationString(lonLat, elevation);
} catch (Exception e) {
return null;
}
}
/**
*
* @param lon
* @param lat
* @return array for longitude and latitude in that order
*/
private double[] convertToWgs84(double lon, double lat) {
if (isWgs84(lon, lat)) {
// valid WGS84 values
logger.info("Found WGS84");
return new double[] { lon, lat };
} else if (isGsk3(lon, lat)) {
// convert from Gsk3 to WGS84
logger.info("Found GSK3");
ProjCoordinate c1 = new ProjCoordinate(lon, lat);
ProjCoordinate c2 = new ProjCoordinate();
gsk3ToWgs84Transform.transform(c1, c2);
return new double[] { c2.x, c2.y };
} else if (isETRS89Utm32N(lon, lat)) {
// convert from UTM32 to WGS84
logger.info("Found UTM32");
ProjCoordinate c1 = new ProjCoordinate(lon, lat);
ProjCoordinate c2 = new ProjCoordinate();
utm32ToWgs84Transform.transform(c1, c2);
return new double[] { c2.x, c2.y };
}
return null;
}
private boolean isWgs84(double lon, double lat) {
return (lat >= -90 && lat <= 90) && (lon >= -180 && lon <= 180);
}
private boolean isGsk3(double lon, double lat) {
// lon=3545359.5355, lat=5462569.635889
return (lon >= 3262472.55 && lon <= 3957072.2) && (lat >= 5236843.08 && lat <= 6123498.88);
}
private boolean isETRS89Utm32N(double lon, double lat) {
// 463996.23 5475743.2
// lon = x = easting
// lat = y = northing
return (lat >= 0 && lat <= 9329005.18) && (lon >= 166021.44 && lon <= 833978.56);
}
private Geolocation findByType(List<Geolocation> data, String type) {
for (Geolocation loc : data) {
if (type.equals(loc.getType())) {
return loc;
}
}
return null;
}
}
package de.hft.stuttgart.lgl.ifc.model;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class Geolocation {
private String type;
@JsonAnySetter
private Map<String, Object> referenceData = new HashMap<>();
public String getType() {
return type;
}
@JsonAnyGetter
public Map<String, Object> getReferenceData() {
return referenceData;
}
@Override
public int hashCode() {
return Objects.hash(referenceData, type);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Geolocation other = (Geolocation) obj;
return Objects.equals(referenceData, other.referenceData) && Objects.equals(type, other.type);
}
@Override
public String toString() {
return "Geolocation [type=" + type + ", referenceData=" + referenceData + "]";
}
}
converter.executable=ifcConvert.exe
python.executable=python.exe
management.endpoints.web.exposure.include=info,health
management.endpoint.health.enabled=true
server.port=50000
......
converter.executable=./IfcConvert
python.executable=python
management.endpoints.web.exposure.include=info,health
management.endpoint.health.enabled=true
server.port=80
......
converter.executable=./IfcConvert
python.executable=python
management.endpoints.web.exposure.include=info,health
management.endpoint.health.enabled=true
server.port=50000
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment