[JAVA] Als ich einen Jersey-API-Test durchführte, stieß ich auf das Phänomen, dass die JSR310-API nicht deserialisiert werden konnte.

Überblick

Als ich eine API mit der DTO-Klasse erstellte, die später als Rückgabewert beschrieben wird, und einen API-Test durchführte, trat die folgende Ausnahme auf und der Test wurde nicht normal beendet.

javax.ws.rs.ProcessingException: Error reading entity from input stream.

	at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:889)
	at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:808)


Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream); line: 1, column: 38](through reference chain: my.controller.MyDto["localDateTime"])
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1452)

Umgebung

Es bestand aus Spring Boot.

Einzelheiten

Da Dto in Kotlins Datenklasse geschrieben wurde, dachte ich, dass es eine Ausnahme war, dass eine neue Instanz nicht mit der newInstance-Methode erstellt werden konnte, da die Eigenschaften des Konstruktors nicht null sein dürfen, aber selbst wenn ich sie wie unten gezeigt in Java umschreibe, ist es dasselbe. Ausnahme aufgetreten.

MyDto.java


package my.controller;

import com.fasterxml.jackson.annotation.JsonAutoDetect;

import java.time.LocalDateTime;

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class MyDto {

    public final String word;
    public final LocalDateTime localDateTime;

    /**
     *Argumentloser Konstruktor für Jackson, um newInstance auszuführen
     */
    private MyDto() {
        this(null, null);
    }

    public MyDto(String word, LocalDateTime localDateTime) {
        this.word = word;
        this.localDateTime = localDateTime;
    }

    public String value() {
        return word + localDateTime.toString();
    }

}

Die Ressourcenklasse, die dieses DTO als Argument verwendet und einen Wert zurückgibt, lautet wie folgt.

MyJerseyResource.java


package my.controller;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.time.LocalDateTime;

@Path("/my")
public class MyJerseyResource {

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public MyDto post(MyDto myDto) {
        return new MyDto(
                myDto.value() + " finishes!",
                LocalDateTime.of(2019, 1, 1, 12, 0, 0)
        );
    }

    @GET
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public MyDto get() {
        return new MyDto(
                "finish!",
                LocalDateTime.of(2019, 1, 1, 12, 0, 0)
        );
    }
}

Wenn sie wie gewohnt als Spring Boot-Anwendung gestartet, mit Post Man usw. ausgeführt werden, können Anforderungen und Antworten normal gesendet und empfangen werden.

Für GET

Screen Shot 2019-06-16 at 7.49.37.png

Für POST

Screen Shot 2019-06-16 at 7.49.29.png

Testklasse

Ich habe eine Testklasse erstellt, indem ich [diese Seite] festgelegt habe (https://qiita.com/kakasak/items/58f3796f05cdc9490845). Der Get-Test endete mit dem obigen Protokoll, aber der Post-Test endete mit einem Status von 400. Die Ergebnisse sind unterschiedlich, aber die Ursache für den Testfehler ist dieselbe wie unten beschrieben.

MyJerseyResourceTest.java


package my.controller;

import my.ConfigureTest;
import my.jersey.MyJerseyTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.time.LocalDateTime;

import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.Is.*;


@RunWith(SpringRunner.class)
@SpringBootTest(classes = {ConfigureTest.class, MyJerseyTest.class})
public class MyJerseyResourceTest {

    @Autowired
    MyJerseyTest jerseyTest;

    @Before
    public void setUp() throws Exception {
        this.jerseyTest.setUp();
    }

    @Test
Testen Sie den öffentlichen ungültigen Beitrag() {
        Response response = jerseyTest.webTarget("/my").request()
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .post(
                        Entity.json(new MyDto(
                                        "start!",
                                        LocalDateTime.of(2018, 1, 1, 12, 1, 1)
                                )
                        )
                );

        assertThat(response.getStatus(), is(200));
        MyDto content = response.readEntity(MyDto.class);
        assertThat(content.word, is("start!2018-01-01T12:01:01 finishes!"));
        assertThat(content.localDateTime.toString(), is(LocalDateTime.of(2019, 1, 1, 12, 0, 0).toString()));
    }

    @Test
Test public void get() {
        Response response = jerseyTest.webTarget("/my").request()
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .get();
        assertThat(response.getStatus(), is(200));

        MyDto content = response.readEntity(MyDto.class);
        assertThat(content.word, is("finish!"));
        assertThat(content.localDateTime.toString(), is(LocalDateTime.of(2019, 1, 1, 12, 0, 0).toString()));
    }
}

Lösung

Die Ursache scheint zu sein, dass der von __Jersey verwendete ObjectMapper JSR310 __ nicht unterstützt. Möglicherweise wählt Jersey standardmäßig Jacksons ObjectMapper als Serializer / Deserializer von Json. Die Konfiguration selbst ist dieselbe wie bei Spring MVC, aber im Gegensatz zum Test von __Spring MVC unterstützt ObjectMapper JSR310 __ nicht, sodass anscheinend ein solcher Fehler auftritt. Um dieses Problem zu beheben, mussten wir einige zusätzliche Einstellungen vornehmen, die nur beim Ausführen von Tests gelten.

Legen Sie ObjectMapper fest, der JSR310 deserialisieren kann

Der im Container mit Spring Boot registrierte ObjectMapper unterstützt JSR310, JerseyTest scheint ihn jedoch nicht zu verwenden. JSR310 Serializer und Deserializer [JavaTimeModule](https://github.com/FasterXML/jackson-datatype-jsr310/blob/master/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule Registrieren Sie sich über .java). Der Serializer und Deserializer, die der JSR310-Klassengruppe entsprechen, sind im Konstruktor dieser Klasse registriert. Zu beachten ist die Registrierung des Deserialisierers. Ich konnte das unten beschriebene JSR310-Deserialisierungsproblem einfach nicht lösen. Daher war es notwendig, einen benutzerdefinierten Deserializer vorzubereiten. Dieses Mal habe ich nur LocalDateTime vorbereitet, aber ich gehe davon aus, dass Sie, wenn Sie andere APIs wie ZonedDateTime verwenden, auch einen Deserializer vorbereiten müssen.

ConfigureTest.java


package my;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Configuration
public class ConfigureTest {

    @Bean
    public ObjectMapper customObjectMapper(Environment environment) {
        JavaTimeModule m = new JavaTimeModule();
        m.addDeserializer(LocalDateTime.class, new CustomLocalDateTimeDeserializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        return Jackson2ObjectMapperBuilder.json()
                .modulesToInstall(m)
                //Wenn Sie es nicht deaktivieren, wird es in Unix Time formatiert.
                .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .featuresToEnable(SerializationFeature.INDENT_OUTPUT)
                .build();

    }
}

Machen Sie sich bereit für die Verwendung eines BeanM ObjectMapper

Ich konnte den ObjectMapper in eine Bean konvertieren, aber dieser Beanized ObjectMapper wird von Jersey nicht verwendet. Es scheint verschiedene Möglichkeiten zu geben, wie dieser ObjectMapper verwendet werden kann, aber ContextResolver ) Um es verfügbar zu machen.

MyJacksonConfigurer.java


package my.jersey;

import com.fasterxml.jackson.databind.ObjectMapper;

import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

@Provider
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class MyJacksonConfigurator implements ContextResolver<ObjectMapper> {

    private final ObjectMapper objectMapper;
    public MyJacksonConfigurator(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return this.objectMapper;
    }
}

Ich bin mir nicht sicher, da es keine Erklärung gibt. Wenn jedoch der im Typparameter des zu implementierenden ContextResolver angegebene Typ in Jersey verwendet wird, wird dieser Kontext aus der Situation über die getContext-Methode dieser Klasse abgerufen. ist. Da der Beanized ObjectMapper in den Konstruktor dieser Klasse eingefügt wird, können Sie den ObjectMapper verwenden, der JSR310 entspricht.

Verarbeiten Sie die Anforderung mit einem Beanized ObjectMapper

Ich habe einen Kontext definiert, aber dieser Kontext ist nicht bei Jersey registriert. Wenn Sie einige Ressourcen für die serverseitige Verarbeitung von Jersey registrieren möchten, müssen Sie sich bei ResourceConfig registrieren und die Einstellungen mit der Konfigurationsmethode JerseyTest wiederherstellen. Durch Registrieren des obigen ContextResolver zusammen mit der Jersey-Ressourcenklasse kann der registrierte ObjectMapper bei der Verarbeitung der Anforderung auf der Serverseite verwendet werden.

MyJerseyTest.java



            @Override
            protected ResourceConfig configure() {
                return new ResourceConfig(MyJerseyResource.class)
                        .register(new MyJacksonConfigurator(objectMapper))
                        .property("contextConfig", applicationContext);
            }

Verarbeiten Sie die Antwort mit einem Bean-basierten ObjectMapper

Jersey scheint streng zwischen serverseitigen und clientseitigen Einstellungen zu unterscheiden. Bei serverseitigen Einstellungen werden Anforderungen verarbeitet. Die Verarbeitung auf der Clientseite ist die Verarbeitung zur Verarbeitung der Antwort. Der Deserializer auf der Serverseite konnte registriert werden, der Deserializer auf der Clientseite konnte jedoch nicht registriert werden. Dies beseitigt den 400-Fehler beim Posten des Testcodes. Wenn ich jedoch versuche, Response.readEntity auszuführen, wird anscheinend ein ObjectMapper verwendet, der JSR310 nicht unterstützt, und der am Anfang angegebene Fehler tritt auf. Um diesen Fehler zu beheben, registrieren Sie einen ContextResolver beim von Jersey verwendeten Client (in diesem Fall die Instanz, die die Anforderung stellt und die Antwort erhält, wenn der Test ausgeführt wird).

MyJerseyTest.java


            @Override
            public Client getClient() {
                return JerseyClientBuilder.createClient()
                        .register(new MyJacksonConfigurator(objectMapper));
            }

Sie haben jetzt einen ObjectMapper für JSR310 registriert. Die Klasse, in der JerseyTest festgelegt ist, lautet wie folgt.

MyJerseyTest.java


package my.jersey;

import com.fasterxml.jackson.databind.ObjectMapper;
import my.controller.MyJerseyResource;
import org.glassfish.jersey.client.JerseyClientBuilder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.ServletDeploymentContext;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.glassfish.jersey.test.spi.TestContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.FormContentFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.filter.RequestContextFilter;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.WebTarget;

@Component
public class MyJerseyTest {

    final JerseyTest jerseyTest;

    boolean start = false;

    public void setUp() throws Exception{
        if (!start) {
            this.jerseyTest.setUp();
        }
        start = true;
    }

    public WebTarget webTarget(String url) {
        return this.jerseyTest.target(url);
    }

    public MyJerseyTest(ApplicationContext applicationContext, ObjectMapper objectMapper) {
        this.jerseyTest = new JerseyTest() {

            @Override
            public Client getClient() {
                return JerseyClientBuilder.createClient()
                        .register(new MyJacksonConfigurator(objectMapper));
            }

            @Override
            protected ResourceConfig configure() {
                return new ResourceConfig(MyJerseyResource.class)
                        .register(new MyJacksonConfigurator(objectMapper))
                        .property("contextConfig", applicationContext);
            }

            @Override
            protected ServletDeploymentContext configureDeployment() {
                return ServletDeploymentContext
                        .forServlet(new ServletContainer(configure()))
                        .addFilter(HiddenHttpMethodFilter.class, HiddenHttpMethodFilter.class.getSimpleName())
                        .addFilter(FormContentFilter.class, FormContentFilter.class.getSimpleName())
                        .addFilter(RequestContextFilter.class, RequestContextFilter.class.getSimpleName())
                        .build();
            }

            @Override
            public TestContainerFactory getTestContainerFactory() {
                return new GrizzlyWebTestContainerFactory();
            }
        };

    }
}

Erstellen Sie einen Deserializer für LocalDateTime

Wie oben erwähnt, habe ich einen LocalDateTime-Deserializer bei ObjectMapper registriert, dieser musste jedoch angepasst werden. Dies liegt daran, dass der Standard-Deserializer die von Jersey generierte Zeichenfolge nicht deserialisieren kann. Es kann mit dem angegebenen Deserializer verarbeitet werden Es mag einen Weg geben, aber ich konnte einfach keine Alternative finden. Für diese Lösung haben wir Folgendes übernommen.

CustomLocalDateDeserializer.java


package my;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class CustomLocalDateTimeDeserializer extends LocalDateTimeDeserializer {

    public CustomLocalDateTimeDeserializer(DateTimeFormatter pattern) {
        super(pattern);
    }

    @Override
    public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {

        if (
                p.hasTokenId(JsonTokenId.ID_STRING) ||
                        p.isExpectedStartArrayToken()) {
            return super.deserialize(p, ctxt);
        } else {
            ObjectNode node = p.getCodec().readTree(p);
            return LocalDateTime.of(
                    node.get("year").asInt(),
                    node.get("monthValue").asInt(),
                    node.get("dayOfMonth").asInt(),
                    node.get("hour").asInt(),
                    node.get("minute").asInt(),
                    node.get("second").asInt()
            );
        }
    }
}

Der Grund dafür ist, dass, wenn die Response.readEntity-Methode in der Testklasse ausgeführt wird, die Zeichenfolge des LocalDateTime-Werts, die vom im innersten Teil ausgeführten Standard-Deserializer Jackson LocalDateTime abgerufen werden kann, lautet Es ist nicht das Format, das Jackson erwartet. Zum Zeitpunkt der Generierung des Antwortobjekts wurde das Format der Zeichenfolge localDateTime unabhängig davon in das folgende Format geändert.

.json


{
	"localDateTime": {
		"dayOfYear": 1,
		"dayOfWeek": "WEDNESDAY",
		"month": "JANUARY",
		"dayOfMonth": 1,
		"year": 2018,
		"monthValue": 1,
		"hour": 1,
		"minute": 0,
		"second": 0,
		"nano": 0,
		"chronology": {
			"id": "ISO",
			"calendarType": "iso8601"
		}
	}
}

Was meinst du? Jedes Mitgliedsobjekt von LocalDateTime wurde sorgfältig in das JSON-Format serialisiert. Anstatt dies zu tun, sollte ich zumindest gehorsam LocalDateTime.toString () ausführen. Sie müssen sich deserialisieren, um diese zusätzliche Sache zu tun. Beim Deserialisieren speichert Jackson die zu deserialisierenden Zeichenfolgeninformationen als Zeichenfolge in JsonParser, dem ersten Argument der Deserialisierungsmethode. Beim Deserialisieren in LocalDateTime wird dieses Format der Zeichenfolge jedoch in LocalDateTime gespeichert. Hat nicht die Fähigkeit zu deserialisieren. Der erste Zweig im obigen Code,

Wenn ein anderes Format als eines der oben genannten vorliegt, tritt eine Ausnahme vom Zitat am Anfang auf. In diesem Fall beginnt die Zeichenfolge anscheinend mit "{", sodass das zu deserialisierende Zeichen als Objekt beurteilt wird und nicht deserialisiert werden kann. [^ 1] Wenn Sie ein Format erhalten, das Jackson nicht unterstützt, müssen Sie es selbst deserialisieren, sodass ich keine andere Wahl hatte, als diese Implementierung zu verwenden. Ich habe mich gefragt, warum es überhaupt in diesem Format erhältlich ist, also habe ich den Jersey-Code getestet, aber festgestellt, dass es zu kompliziert ist und Zeit verschwendet, um es zu entschlüsseln, sodass ich nicht tiefer gegangen bin. .. ..

Impressionen

Spring Boot macht viel Arbeit, aber es scheint, dass einige Teile von Jersey unerreichbar sind. Mit Spring MVC können Sie nahtlos mit Spring Boot testen, sodass dieses Problem nicht auftritt. Sie können testen, ohne an etwas zu denken. Und JAX-RS scheint Sie zu zwingen, die Details der Spezifikation zu verstehen, bevor Sie sie verwenden. Die uns zur Verfügung stehende Zeit ist begrenzt. Wenn Sie Ihre Zeit damit verbringen, sich an etwas zu erinnern, das nicht getan werden kann, möchten Sie Ihre Zeit damit verbringen, andere Dinge zu tun. Ursprünglich sollte das Framework sein Bestes tun, um diese Zeit zu verkürzen, aber es ist auch ein guter Punkt, um umzufallen. Als ich mich für Spring Boot entschied, wurde mir klar, dass die Wahl von Jersey (JAX-RS) keinen Wert hatte. ~~ Erstens sind die Vorteile der Erstellung einer anderen Webanwendung als Spring Boot mit Java derzeit nicht klar, sodass JAX-RS anscheinend nicht erforderlich ist. ~~

Ergänzung

Ich habe zwei andere Methoden als die Verwendung des Beanized ObjectMapper mit ContextResolver gefunden, daher werde ich sie aufschreiben. In beiden Fällen muss die Instanz auf der Server- und der Client-Seite registriert werden.

Verwenden Sie JacksonJaxbJsonProvider

Standardmäßig wird ObjectMapper zum Serialisieren und Deserialisieren von Json von Jersey verwendet. Es scheint jedoch, dass dieses Verhalten mit einer Klasse namens JacksonJaxbJsonProvider erreicht wird. Überschreiben Sie daher diese Klasse.

CustomJacksonJsonProvider.java


package my;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;

import javax.annotation.Priority;
import javax.ws.rs.ext.Provider;


@Provider
@Priority(0)
public class CustomJacksonJsonProvider extends JacksonJaxbJsonProvider {
    public CustomJacksonJsonProvider(ObjectMapper objectMapper) {
        super();
        super.setMapper(objectMapper);
    }
}

Implementieren Sie MessageBodyReader und MessageBodyWrite

Der Körperinhalt, der in die Jersey-Antwort einfließt, scheint in die Implementierung der Titelklasse zu fließen. Es scheint den Körper zu lesen und zu schreiben, indem diese Klasse gelöst wird, und Sie können hier Serialisierung und Deserialisierung einbinden. Diese Methode wird jedoch überhaupt nicht empfohlen, da sie eine zusätzliche Implementierung erfordert, kompliziert ist und unklar ist, ob sie überhaupt wirklich praktikabel ist. Nur als Referenz.

MyMessageBody.java


package my;

import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

@Provider
@Produces(value ={MediaType.APPLICATION_JSON})
@Consumes(value ={MediaType.APPLICATION_JSON})
public class MyMessageBody<T extends Serializable> implements MessageBodyReader<T>, MessageBodyWriter<T> {

    @Override
    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return true;
    }

    @Override
    public T readFrom(Class<T> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
        //Zu serialisierender Inhalt
        return null;
    }

    @Override
    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return true;
    }

    @Override
    public void writeTo(T t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
        //Inhalt zu deserialisieren

    }
}

[^ 1]: Nebenbei bemerkt verfügt JsonParser über die Informationen, um sich selbst als Zeichenfolge zu deserialisieren. Wenn die Zeichenfolge von JsonParse nicht mit "{", "[" beginnt, handelt es sich um eine Zeichenfolge (ID_STRING). Wenn sie mit "{" beginnt, handelt es sich um ein Objekt (START_OBJECT). Wenn sie mit "[" beginnt, handelt es sich um ein Array ( Es scheint als START_ARRAY) beurteilt zu werden.

Recommended Posts

Als ich einen Jersey-API-Test durchführte, stieß ich auf das Phänomen, dass die JSR310-API nicht deserialisiert werden konnte.
Die Geschichte des Anhaltens, weil der Schienentest nicht durchgeführt werden konnte
Nachdem ich das Montyhall-Problem mit Ruby überprüft hatte, war es eine Geschichte, die ich gut verstehen konnte und die ich nicht gut verstand
Die Geschichte, die ich nach der Installation mehrerer Java unter Windows nicht erstellen konnte