parent
3d00d83fc6
commit
8bf7e346ee
|
|
@ -130,4 +130,6 @@ public class ComponentsDefine {
|
|||
|
||||
public static final OfficialComponent SPRING_WEBFLUX = new OfficialComponent(67, "spring-webflux");
|
||||
|
||||
public static final OfficialComponent PLAY = new OfficialComponent(68, "Play");
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.agent.core.plugin.bytebuddy;
|
||||
|
||||
import net.bytebuddy.description.annotation.AnnotationDescription;
|
||||
import net.bytebuddy.description.annotation.AnnotationSource;
|
||||
import net.bytebuddy.matcher.CollectionItemMatcher;
|
||||
import net.bytebuddy.matcher.DeclaringAnnotationMatcher;
|
||||
import net.bytebuddy.matcher.ElementMatcher;
|
||||
|
||||
/**
|
||||
* Annotation Type match.
|
||||
* Similar with {@link net.bytebuddy.matcher.ElementMatchers#isAnnotatedWith},
|
||||
* the only different between them is this match use {@link String} to declare the type, instead of {@link Class}.
|
||||
* This can avoid the classloader risk.
|
||||
* <p>
|
||||
*
|
||||
* @author AI
|
||||
* 2019-08-15
|
||||
*/
|
||||
public class AnnotationTypeNameMatch<T extends AnnotationDescription> implements ElementMatcher<T> {
|
||||
|
||||
/**
|
||||
* the target annotation type
|
||||
*/
|
||||
private String annotationTypeName;
|
||||
|
||||
/**
|
||||
* declare the match target method with the certain type.
|
||||
*
|
||||
* @param annotationTypeName target annotation type
|
||||
*/
|
||||
private AnnotationTypeNameMatch(String annotationTypeName) {
|
||||
this.annotationTypeName = annotationTypeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(T target) {
|
||||
return target.getAnnotationType().asErasure().getName().equals(annotationTypeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* The static method to create {@link AnnotationTypeNameMatch}
|
||||
* This is a delegate method to follow byte-buddy {@link ElementMatcher}'s code style.
|
||||
*
|
||||
* @param annotationTypeName target annotation type
|
||||
* @param <T> The type of the object that is being matched.
|
||||
* @return new {@link AnnotationTypeNameMatch} instance.
|
||||
*/
|
||||
public static <T extends AnnotationSource> ElementMatcher.Junction<T> isAnnotatedWithType(String annotationTypeName) {
|
||||
final AnnotationTypeNameMatch<AnnotationDescription> matcher = new AnnotationTypeNameMatch<AnnotationDescription>(annotationTypeName);
|
||||
return new DeclaringAnnotationMatcher<T>(new CollectionItemMatcher<AnnotationDescription>(matcher));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.agent.core.plugin.bytebuddy;
|
||||
|
||||
import net.bytebuddy.description.method.MethodDescription;
|
||||
import net.bytebuddy.matcher.ElementMatcher;
|
||||
|
||||
|
||||
/**
|
||||
* Return Type match.
|
||||
* Similar with {@link net.bytebuddy.matcher.ElementMatchers#returns},
|
||||
* the only different between them is this match use {@link String} to declare the type, instead of {@link Class}.
|
||||
* This can avoid the classloader risk.
|
||||
* <p>
|
||||
*
|
||||
* @author AI
|
||||
* 2019-08-15
|
||||
*/
|
||||
public class ReturnTypeNameMatch implements ElementMatcher<MethodDescription> {
|
||||
|
||||
/**
|
||||
* the target return type
|
||||
*/
|
||||
private String returnTypeName;
|
||||
|
||||
/**
|
||||
* declare the match target method with the certain type.
|
||||
*
|
||||
* @param returnTypeName target return type
|
||||
*/
|
||||
private ReturnTypeNameMatch(String returnTypeName) {
|
||||
this.returnTypeName = returnTypeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(MethodDescription target) {
|
||||
return target.getReturnType().asErasure().getName().equals(returnTypeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* The static method to create {@link ReturnTypeNameMatch}
|
||||
* This is a delegate method to follow byte-buddy {@link ElementMatcher}'s code style.
|
||||
*
|
||||
* @param returnTypeName target return type
|
||||
* @return new {@link ReturnTypeNameMatch} instance.
|
||||
*/
|
||||
public static ElementMatcher<MethodDescription> returnsWithType(String returnTypeName) {
|
||||
return new ReturnTypeNameMatch(returnTypeName);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.agent.core.plugin.bytebuddy;
|
||||
|
||||
import net.bytebuddy.description.method.MethodDescription;
|
||||
import net.bytebuddy.matcher.ElementMatcher;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* @author AI
|
||||
* 2019-08-15
|
||||
*/
|
||||
public class AnnotationTypeNameMatchTest {
|
||||
|
||||
@Test
|
||||
public void testMatch() throws Exception {
|
||||
final ElementMatcher<MethodDescription> matcher = AnnotationTypeNameMatch.isAnnotatedWithType("org.apache.skywalking.apm.agent.core.plugin.bytebuddy.Inject");
|
||||
Assert.assertTrue(matcher.matches(new MethodDescription.ForLoadedConstructor(Person.class.getConstructor(String.class))));
|
||||
Assert.assertFalse(matcher.matches(new MethodDescription.ForLoadedConstructor(Person.class.getConstructor(String.class, int.class))));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.agent.core.plugin.bytebuddy;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* @author AI
|
||||
* 2019-08-15
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Inject {
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.agent.core.plugin.bytebuddy;
|
||||
|
||||
/**
|
||||
* @author AI
|
||||
* 2019-08-15
|
||||
*/
|
||||
public class Person {
|
||||
private int age;
|
||||
private String name;
|
||||
|
||||
@Inject
|
||||
public Person(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Person(String name, int age) {
|
||||
this.age = age;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.agent.core.plugin.bytebuddy;
|
||||
|
||||
import net.bytebuddy.description.method.MethodDescription;
|
||||
import net.bytebuddy.matcher.ElementMatcher;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* @author AI
|
||||
* 2019-08-15
|
||||
*/
|
||||
public class ReturnTypeNameMatchTest {
|
||||
|
||||
@Test
|
||||
public void testMatch() throws Exception {
|
||||
final ElementMatcher<MethodDescription> matcher = ReturnTypeNameMatch.returnsWithType("java.lang.String");
|
||||
Assert.assertTrue(matcher.matches(new MethodDescription.ForLoadedMethod(Person.class.getMethod("getName"))));
|
||||
Assert.assertFalse(matcher.matches(new MethodDescription.ForLoadedMethod(Person.class.getMethod("getAge"))));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
~ contributor license agreements. See the NOTICE file distributed with
|
||||
~ this work for additional information regarding copyright ownership.
|
||||
~ The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
~ (the "License"); you may not use this file except in compliance with
|
||||
~ the License. You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
~
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.apache.skywalking</groupId>
|
||||
<artifactId>optional-plugins</artifactId>
|
||||
<version>6.4.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>apm-play-2.x-plugin</artifactId>
|
||||
<name>play-2.x-plugin</name>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<play2.version>2.7.3</play2.version>
|
||||
<compiler.version>1.8</compiler.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.typesafe.play</groupId>
|
||||
<artifactId>play_2.12</artifactId>
|
||||
<version>${play2.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.plugin.play.v2x;
|
||||
|
||||
import org.apache.skywalking.apm.agent.core.context.ContextManager;
|
||||
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
|
||||
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor;
|
||||
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
|
||||
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
|
||||
import play.api.inject.Injector;
|
||||
import scala.collection.Seq;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author AI
|
||||
* 2019-08-01
|
||||
*/
|
||||
public class HttpFiltersInterceptor implements InstanceMethodsAroundInterceptor, InstanceConstructorInterceptor {
|
||||
|
||||
@Override
|
||||
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) throws Throwable {
|
||||
Object object = objInst.getSkyWalkingDynamicField();
|
||||
Injector injector = (Injector) object;
|
||||
TracingFilter filter = injector.instanceOf(TracingFilter.class);
|
||||
Seq seq = (Seq) ret;
|
||||
List<Object> filters = new ArrayList<>(seq.size() + 1);
|
||||
filters.add(filter);
|
||||
filters.addAll(scala.collection.JavaConverters.asJavaCollection(seq));
|
||||
return scala.collection.JavaConverters.asScalaBuffer(filters).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
|
||||
ContextManager.activeSpan().errorOccurred().log(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConstruct(EnhancedInstance objInst, Object[] allArguments) {
|
||||
objInst.setSkyWalkingDynamicField(allArguments[2]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.plugin.play.v2x;
|
||||
|
||||
import akka.stream.Materializer;
|
||||
import org.apache.skywalking.apm.agent.core.context.CarrierItem;
|
||||
import org.apache.skywalking.apm.agent.core.context.ContextCarrier;
|
||||
import org.apache.skywalking.apm.agent.core.context.ContextManager;
|
||||
import org.apache.skywalking.apm.agent.core.context.tag.Tags;
|
||||
import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
|
||||
import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
|
||||
import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
|
||||
import play.api.routing.HandlerDef;
|
||||
import play.mvc.Filter;
|
||||
import play.mvc.Http;
|
||||
import play.mvc.Result;
|
||||
import play.routing.Router;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author AI
|
||||
* 2019-08-01
|
||||
*/
|
||||
@Singleton
|
||||
public class TracingFilter extends Filter {
|
||||
|
||||
private final Pattern routePattern = Pattern.compile("\\$(\\w+)\\<\\[\\^/\\]\\+\\>", Pattern.DOTALL);
|
||||
|
||||
@Inject
|
||||
public TracingFilter(Materializer mat) {
|
||||
super(mat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Result> apply(Function<Http.RequestHeader, CompletionStage<Result>> next, Http.RequestHeader request) {
|
||||
HandlerDef def = null;
|
||||
try {
|
||||
def = request.attrs().get(Router.Attrs.HANDLER_DEF);
|
||||
} catch (Throwable t) {
|
||||
// ignore get HandlerDef exception
|
||||
}
|
||||
if (Objects.nonNull(def)) {
|
||||
final ContextCarrier carrier = new ContextCarrier();
|
||||
CarrierItem items = carrier.items();
|
||||
while (items.hasNext()) {
|
||||
items = items.next();
|
||||
Optional<String> value = request.getHeaders().get(items.getHeadKey());
|
||||
if (value.isPresent()) {
|
||||
items.setHeadValue(value.get());
|
||||
}
|
||||
}
|
||||
final String operationName = routePattern.matcher(def.path()).replaceAll("{$1}");
|
||||
final AbstractSpan span = ContextManager.createEntrySpan(operationName, carrier);
|
||||
final String url = request.host() + request.uri();
|
||||
Tags.URL.set(span, url);
|
||||
Tags.HTTP.METHOD.set(span, request.method());
|
||||
span.setComponent(ComponentsDefine.PLAY);
|
||||
SpanLayer.asHttp(span);
|
||||
span.prepareForAsync();
|
||||
CompletionStage<Result> stage = next.apply(request).thenApply(result -> {
|
||||
if (result.status() >= 400) {
|
||||
span.errorOccurred();
|
||||
Tags.STATUS_CODE.set(span, Integer.toString(result.status()));
|
||||
}
|
||||
try {
|
||||
span.asyncFinish();
|
||||
} catch (Throwable t) {
|
||||
ContextManager.activeSpan().errorOccurred().log(t);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
ContextManager.stopSpan(span);
|
||||
return stage;
|
||||
}
|
||||
return next.apply(request);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.plugin.play.v2x.define;
|
||||
|
||||
import net.bytebuddy.description.method.MethodDescription;
|
||||
import net.bytebuddy.matcher.ElementMatcher;
|
||||
import org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ReturnTypeNameMatch;
|
||||
import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint;
|
||||
import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint;
|
||||
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine;
|
||||
import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
|
||||
|
||||
import static net.bytebuddy.matcher.ElementMatchers.named;
|
||||
import static org.apache.skywalking.apm.agent.core.plugin.bytebuddy.AnnotationTypeNameMatch.isAnnotatedWithType;
|
||||
import static org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType;
|
||||
import static org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName;
|
||||
|
||||
/**
|
||||
* @author AI
|
||||
* 2019-08-01
|
||||
*/
|
||||
public class Play2xInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
|
||||
private static final String ENHANCE_CLASS = "play.api.http.EnabledFilters";
|
||||
private static final String ENHANCE_METHOD = "filters";
|
||||
private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.play.v2x.HttpFiltersInterceptor";
|
||||
|
||||
@Override
|
||||
protected ClassMatch enhanceClass() {
|
||||
return byName(ENHANCE_CLASS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
|
||||
return new ConstructorInterceptPoint[]{
|
||||
new ConstructorInterceptPoint() {
|
||||
@Override
|
||||
public ElementMatcher<MethodDescription> getConstructorMatcher() {
|
||||
return Play2xInstrumentation.getInjectConstructorMatcher();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConstructorInterceptor() {
|
||||
return INTERCEPTOR_CLASS;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
|
||||
return new InstanceMethodsInterceptPoint[]{
|
||||
new InstanceMethodsInterceptPoint() {
|
||||
@Override
|
||||
public ElementMatcher<MethodDescription> getMethodsMatcher() {
|
||||
return Play2xInstrumentation.getFiltersMethodMatcher();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodsInterceptor() {
|
||||
return INTERCEPTOR_CLASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOverrideArgs() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ElementMatcher<MethodDescription> getInjectConstructorMatcher() {
|
||||
return isAnnotatedWithType("javax.inject.Inject")
|
||||
.and(takesArgumentWithType(2, "play.api.inject.Injector"));
|
||||
}
|
||||
|
||||
public static ElementMatcher<MethodDescription> getFiltersMethodMatcher() {
|
||||
String scala212Seq = "scala.collection.Seq";
|
||||
String scala213Seq = "scala.collection.immutable.Seq";
|
||||
return (named(ENHANCE_METHOD).and(ReturnTypeNameMatch.returnsWithType(scala212Seq)))
|
||||
.or(named(ENHANCE_METHOD).and(ReturnTypeNameMatch.returnsWithType(scala213Seq)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
play-2.x=org.apache.skywalking.apm.plugin.play.v2x.define.Play2xInstrumentation
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.plugin.play.v2x;
|
||||
|
||||
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import play.api.inject.BindingKey;
|
||||
import play.api.inject.Injector;
|
||||
import scala.collection.immutable.Seq;
|
||||
import scala.collection.immutable.Seq$;
|
||||
import scala.reflect.ClassTag;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author AI
|
||||
* 2019-08-07
|
||||
*/
|
||||
public class HttpFiltersInterceptorTest {
|
||||
|
||||
private EnhancedInstance enhancedInstance = new EnhancedInstance() {
|
||||
private Object object = null;
|
||||
|
||||
@Override
|
||||
public Object getSkyWalkingDynamicField() {
|
||||
return object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSkyWalkingDynamicField(Object value) {
|
||||
this.object = value;
|
||||
}
|
||||
};
|
||||
|
||||
private HttpFiltersInterceptor interceptor = new HttpFiltersInterceptor();
|
||||
private Injector injector = new Injector() {
|
||||
@Override
|
||||
public <T> T instanceOf(ClassTag<T> evidence) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T instanceOf(Class<T> clazz) {
|
||||
return (T) new TracingFilter(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T instanceOf(BindingKey<T> key) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public play.inject.Injector asJava() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testBindingInjector() {
|
||||
Object[] arguments = new Object[]{null, null, injector};
|
||||
interceptor.onConstruct(enhancedInstance, arguments);
|
||||
Assert.assertTrue(Objects.nonNull(enhancedInstance.getSkyWalkingDynamicField()));
|
||||
Assert.assertTrue(enhancedInstance.getSkyWalkingDynamicField() instanceof Injector);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturningTracingFilter() throws Throwable {
|
||||
Seq ret = Seq$.MODULE$.empty();
|
||||
enhancedInstance.setSkyWalkingDynamicField(injector);
|
||||
Object result = interceptor.afterMethod(enhancedInstance, null, null, null, ret);
|
||||
Assert.assertTrue(Objects.nonNull(result));
|
||||
Seq filters = (Seq) result;
|
||||
Assert.assertTrue(filters.head() instanceof TracingFilter);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.plugin.play.v2x;
|
||||
|
||||
import net.bytebuddy.description.method.MethodDescription;
|
||||
import net.bytebuddy.matcher.ElementMatcher;
|
||||
import org.apache.skywalking.apm.plugin.play.v2x.define.Play2xInstrumentation;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import play.api.http.EnabledFilters;
|
||||
|
||||
/**
|
||||
* @author AI
|
||||
* 2019-08-15
|
||||
*/
|
||||
public class Play2xInstrumentationTest {
|
||||
|
||||
@Test
|
||||
public void testConstructorMatch() throws Exception {
|
||||
final ElementMatcher<MethodDescription> matcher = Play2xInstrumentation.getInjectConstructorMatcher();
|
||||
final MethodDescription method = new MethodDescription.ForLoadedConstructor(
|
||||
EnabledFilters.class.getConstructor(play.api.Environment.class, play.api.Configuration.class, play.api.inject.Injector.class)
|
||||
);
|
||||
Assert.assertTrue(matcher.matches(method));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodMatch() throws Exception {
|
||||
final ElementMatcher<MethodDescription> matcher = Play2xInstrumentation.getFiltersMethodMatcher();
|
||||
final MethodDescription method = new MethodDescription.ForLoadedMethod(EnabledFilters.class.getMethod("filters"));
|
||||
Assert.assertTrue(matcher.matches(method));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package org.apache.skywalking.apm.plugin.play.v2x;
|
||||
|
||||
import akka.stream.Materializer;
|
||||
import org.apache.skywalking.apm.agent.core.context.trace.AbstractTracingSpan;
|
||||
import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
|
||||
import org.apache.skywalking.apm.agent.core.context.trace.TraceSegment;
|
||||
import org.apache.skywalking.apm.agent.test.helper.SegmentHelper;
|
||||
import org.apache.skywalking.apm.agent.test.helper.SpanHelper;
|
||||
import org.apache.skywalking.apm.agent.test.tools.AgentServiceRule;
|
||||
import org.apache.skywalking.apm.agent.test.tools.SegmentStorage;
|
||||
import org.apache.skywalking.apm.agent.test.tools.SegmentStoragePoint;
|
||||
import org.apache.skywalking.apm.agent.test.tools.SpanAssert;
|
||||
import org.apache.skywalking.apm.agent.test.tools.TracingSegmentRunner;
|
||||
import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
import org.powermock.modules.junit4.PowerMockRunnerDelegate;
|
||||
import play.api.http.MediaRange;
|
||||
import play.api.mvc.RequestHeader;
|
||||
import play.api.routing.HandlerDef;
|
||||
import play.i18n.Lang;
|
||||
import play.libs.typedmap.TypedKey;
|
||||
import play.libs.typedmap.TypedMap;
|
||||
import play.mvc.Http;
|
||||
import play.mvc.Result;
|
||||
import play.routing.Router;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.apache.skywalking.apm.agent.test.tools.SpanAssert.assertComponent;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static play.mvc.Results.badRequest;
|
||||
import static play.mvc.Results.ok;
|
||||
|
||||
|
||||
/**
|
||||
* @author AI
|
||||
* 2019-08-16
|
||||
*/
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PowerMockRunnerDelegate(TracingSegmentRunner.class)
|
||||
public class TracingFilterTest {
|
||||
|
||||
@SegmentStoragePoint
|
||||
private SegmentStorage segmentStorage;
|
||||
@Rule
|
||||
public AgentServiceRule serviceRule = new AgentServiceRule();
|
||||
|
||||
@Mock
|
||||
private Materializer materializer;
|
||||
|
||||
private Http.RequestHeader request = new Http.RequestHeader() {
|
||||
@Override
|
||||
public String uri() {
|
||||
return "/projects/1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String method() {
|
||||
return "GET";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String version() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String remoteAddress() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean secure() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypedMap attrs() {
|
||||
HandlerDef def = new HandlerDef(null, null, null, "GET",
|
||||
null, null, "/projects/$projectId<[^/]+>", null, null);
|
||||
return TypedMap.empty().put(Router.Attrs.HANDLER_DEF, def);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Http.RequestHeader withAttrs(TypedMap typedMap) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A> Http.RequestHeader addAttr(TypedKey<A> typedKey, A a) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Http.RequestHeader removeAttr(TypedKey<?> typedKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Http.Request withBody(Http.RequestBody requestBody) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String host() {
|
||||
return "localhost:8080";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String path() {
|
||||
return "/projects/1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Lang> acceptLanguages() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MediaRange> acceptedTypes() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accepts(String s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String[]> queryString() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQueryString(String s) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Http.Cookies cookies() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Http.Cookie cookie(String s) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Http.Headers getHeaders() {
|
||||
return new Http.Headers(new HashMap<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasBody() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> contentType() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> charset() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<List<X509Certificate>> clientCertificateChain() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeader asScala() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testStatusCodeIsOk() throws Exception {
|
||||
TracingFilter filter = new TracingFilter(materializer);
|
||||
Function<Http.RequestHeader, CompletionStage<Result>> next = requestHeader -> CompletableFuture.supplyAsync(() -> ok("Hello"));
|
||||
CompletionStage<Result> result = filter.apply(next, request);
|
||||
result.toCompletableFuture().get();
|
||||
assertThat(segmentStorage.getTraceSegments().size(), is(1));
|
||||
TraceSegment traceSegment = segmentStorage.getTraceSegments().get(0);
|
||||
List<AbstractTracingSpan> spans = SegmentHelper.getSpans(traceSegment);
|
||||
assertHttpSpan(spans.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatusCodeIsNotOk() throws Exception {
|
||||
TracingFilter filter = new TracingFilter(materializer);
|
||||
Function<Http.RequestHeader, CompletionStage<Result>> next = requestHeader -> CompletableFuture.supplyAsync(() -> badRequest("Hello"));
|
||||
CompletionStage<Result> result = filter.apply(next, request);
|
||||
result.toCompletableFuture().get();
|
||||
assertThat(segmentStorage.getTraceSegments().size(), is(1));
|
||||
TraceSegment traceSegment = segmentStorage.getTraceSegments().get(0);
|
||||
List<AbstractTracingSpan> spans = SegmentHelper.getSpans(traceSegment);
|
||||
assertHttpSpan(spans.get(0));
|
||||
assertThat(SpanHelper.getErrorOccurred(spans.get(0)), is(true));
|
||||
}
|
||||
|
||||
private void assertHttpSpan(AbstractTracingSpan span) {
|
||||
assertThat(span.getOperationName(), is("/projects/{projectId}"));
|
||||
assertComponent(span, ComponentsDefine.PLAY);
|
||||
SpanAssert.assertTag(span, 0, "localhost:8080/projects/1");
|
||||
assertThat(span.isEntry(), is(true));
|
||||
SpanAssert.assertLayer(span, SpanLayer.HTTP);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -47,6 +47,7 @@
|
|||
<module>lettuce-5.x-plugin</module>
|
||||
<module>zookeeper-3.4.x-plugin</module>
|
||||
<module>customize-enhance-plugin</module>
|
||||
<module>play-2.x-plugin</module>
|
||||
</modules>
|
||||
|
||||
<dependencies>
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ Now, we have the following known optional plugins.
|
|||
* Plugin of Zookeeper 3.4.x in optional plugin folder. The reason of being optional plugin is, many business irrelevant traces are generated, which cause extra payload to agents and backends. At the same time, those traces may be just heartbeat(s).
|
||||
* [Customize enhance](Customize-enhance-trace.md) Trace methods based on description files, rather than write plugin or change source codes.
|
||||
* Plugin of Spring Cloud Gateway 2.1.x in optional plugin folder. Please only active this plugin when you install agent in Spring Gateway.
|
||||
* Plugin of [Play Framework](https://www.playframework.com/) 2.6+ (JDK 1.8 required & Scala 2.12/2.13) in optional plugin folder. Please only active this plugin when you install agent in [Play Framework](https://www.playframework.com/).
|
||||
|
||||
## Bootstrap class plugins
|
||||
All bootstrap plugins are optional, due to unexpected risk. Bootstrap plugins are provided in `bootstrap-plugins` folder.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
* [Spring Webflux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html) 5.x
|
||||
* [Undertow](http://undertow.io/) 2.0.0.Final -> 2.0.13.Final
|
||||
* [RESTEasy](https://resteasy.github.io/) 3.1.0.Final -> 3.7.0.Final
|
||||
* [Play Framework](https://www.playframework.com/) 2.6.x -> 2.7.x (Optional²)
|
||||
* HTTP Client
|
||||
* [Feign](https://github.com/OpenFeign/feign) 9.x
|
||||
* [Netflix Spring Cloud Feign](https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-starter-feign) 1.1.x, 1.2.x, 1.3.x
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ or providing commercial products including Apache SkyWalking.
|
|||
1. GOME 国美 https://www.gome.com.cn/
|
||||
1. Guazi.com 瓜子二手车直卖网. https://www.guazi.com/
|
||||
1. guohuaitech.com 北京国槐信息科技有限公司. http://www.guohuaitech.com/
|
||||
1. GrowingIO 北京易数科技有限公司 https://www.growingio.com/
|
||||
1. Haier. 海尔消费金融 https://www.haiercash.com/
|
||||
1. Haoyunhu. 上海好运虎供应链管理有限公司 http://www.haoyunhu56.com/
|
||||
1. Huawei Inc. DevCloud. https://www.huaweicloud.com/devcloud/
|
||||
|
|
|
|||
|
|
@ -212,6 +212,9 @@ JdkHttp:
|
|||
spring-webflux:
|
||||
id: 67
|
||||
languages: Java
|
||||
Play:
|
||||
id: 68
|
||||
languages: Java,Scala
|
||||
|
||||
# .NET/.NET Core components
|
||||
# [3000, 4000) for C#/.NET only
|
||||
|
|
|
|||
|
|
@ -230,6 +230,9 @@ JdkHttp:
|
|||
spring-webflux:
|
||||
id: 67
|
||||
languages: Java
|
||||
Play:
|
||||
id: 68
|
||||
languages: Java,Scala
|
||||
|
||||
|
||||
# .NET/.NET Core components
|
||||
|
|
|
|||
Loading…
Reference in New Issue