Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement @Attribute Injection. #5547

Merged
merged 41 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
eac6431
skeleton code for @Attribute injection.
chickenchickenlove Mar 29, 2024
2801cdd
Implement Attribute Injection : Apply review
chickenchickenlove Apr 1, 2024
889d887
make test code for Attribute injection.
chickenchickenlove Apr 1, 2024
94a93af
remove duplicate code from AnnotatedValueResolverTest
chickenchickenlove Apr 1, 2024
42ef9a2
Implement Attribute Injection : Apply review2
chickenchickenlove Apr 2, 2024
ab8a08a
Implement Attribute Injection : Apply review3
chickenchickenlove Apr 3, 2024
3fac84e
Implement Attribute Injection : Apply review3. remove useless comments.
chickenchickenlove Apr 3, 2024
f3f8b74
Fix lint, Apply review
chickenchickenlove Apr 3, 2024
821e18f
Add user docs
chickenchickenlove Apr 3, 2024
dce9520
Update core/src/main/java/com/linecorp/armeria/server/annotation/Attr…
chickenchickenlove Apr 5, 2024
fc5771a
Update core/src/main/java/com/linecorp/armeria/server/annotation/Attr…
chickenchickenlove Apr 5, 2024
f017563
Update core/src/main/java/com/linecorp/armeria/server/annotation/Attr…
chickenchickenlove Apr 5, 2024
1927e46
Update core/src/main/java/com/linecorp/armeria/server/annotation/Attr…
chickenchickenlove Apr 5, 2024
ee7a466
Use @default.class and fix link of java docs.
chickenchickenlove Apr 5, 2024
65868b7
Merge branch 'main' into 240329-try1
chickenchickenlove Apr 5, 2024
1d0b6c7
fix lint error
chickenchickenlove Apr 5, 2024
1877117
Update site/src/pages/docs/server-annotated-service.mdx
chickenchickenlove Apr 9, 2024
bdb0088
Update site/src/pages/docs/server-annotated-service.mdx
chickenchickenlove Apr 9, 2024
29543f9
Apply comment to server-annotated-service docs.
chickenchickenlove Apr 9, 2024
957378f
Update core/src/main/java/com/linecorp/armeria/server/annotation/Attr…
chickenchickenlove Apr 12, 2024
2893d3d
modify misstypo
chickenchickenlove Apr 12, 2024
c70c2fb
remove findName().
chickenchickenlove Apr 12, 2024
c1de466
Throw ClassCastException on AttributeResolver.
chickenchickenlove Apr 12, 2024
e6632fa
modify miss typoe
chickenchickenlove Apr 17, 2024
065e43a
Apply code review
chickenchickenlove Apr 17, 2024
e80075f
apply code review
chickenchickenlove Apr 18, 2024
5874745
apply review
chickenchickenlove Apr 18, 2024
c6519ab
apply review
chickenchickenlove Apr 18, 2024
bb087a7
apply review
chickenchickenlove Apr 18, 2024
4560943
apply review
chickenchickenlove Apr 22, 2024
0dd3e77
Update core/src/main/java/com/linecorp/armeria/server/annotation/Attr…
chickenchickenlove Apr 23, 2024
65bc886
throw immedetely if valued is failed to be cast.
chickenchickenlove Apr 23, 2024
c27629b
Update core/src/main/java/com/linecorp/armeria/internal/server/annota…
chickenchickenlove Apr 24, 2024
3943f70
Update core/src/main/java/com/linecorp/armeria/internal/server/annota…
chickenchickenlove Apr 24, 2024
3df348b
Update core/src/test/java/com/linecorp/armeria/internal/server/annota…
chickenchickenlove Apr 24, 2024
7230534
apply review
chickenchickenlove Apr 24, 2024
72d26b2
fix lint error.
chickenchickenlove Apr 24, 2024
8f3631c
Update core/src/main/java/com/linecorp/armeria/server/annotation/Attr…
chickenchickenlove May 2, 2024
9013388
apply review
chickenchickenlove May 2, 2024
588c8bb
Merge branch 'main' into 240329-try1
chickenchickenlove May 7, 2024
70d76cb
fix lint error
chickenchickenlove May 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,52 +21,29 @@
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.base.CaseFormat;

import com.linecorp.armeria.server.annotation.Attribute;
import com.linecorp.armeria.server.annotation.Header;
import com.linecorp.armeria.server.annotation.Param;

final class AnnotatedElementNameUtil {

/**
* Returns the value of the {@link Param} annotation which is specified on the {@code element} if
* the value is not blank. If the value is blank, it returns the name of the specified
* {@code nameRetrievalTarget} object which is an instance of {@link Parameter} or {@link Field}.
* Returns the value of {@link Header}, {@link Param}, {@link Attribute} if the value is not blank.
* If the value is blank, it returns the name of the specified {@code nameRetrievalTarget} object
* which is an instance of {@link Header}, {@link Param}, {@link Attribute} or {@link Field}.
*/
static String findName(Param param, Object nameRetrievalTarget) {
static String findName(Object nameRetrievalTarget, String value) {
requireNonNull(nameRetrievalTarget, "nameRetrievalTarget");

final String value = param.value();
if (DefaultValues.isSpecified(value)) {
checkArgument(!value.isEmpty(), "value is empty.");
return value;
}
return getName(nameRetrievalTarget);
}

/**
* Returns the value of the {@link Header} annotation which is specified on the {@code element} if
* the value is not blank. If the value is blank, it returns the name of the specified
* {@code nameRetrievalTarget} object which is an instance of {@link Parameter} or {@link Field}.
*
* <p>Note that the name of the specified {@code nameRetrievalTarget} will be converted as
* {@link CaseFormat#LOWER_HYPHEN} that the string elements are separated with one hyphen({@code -})
* character. The value of the {@link Header} annotation will not be converted because it is clearly
* specified by a user.
*/
static String findName(Header header, Object nameRetrievalTarget) {
requireNonNull(nameRetrievalTarget, "nameRetrievalTarget");

final String value = header.value();
if (DefaultValues.isSpecified(value)) {
checkArgument(!value.isEmpty(), "value is empty.");
return value;
}
return toHeaderName(getName(nameRetrievalTarget));
}

/**
* Returns the name of the specified element or the default name if it can't get.
*/
Expand All @@ -89,8 +66,7 @@ static String getName(Object element) {
throw new IllegalArgumentException(
"cannot obtain the name of the parameter or field automatically. " +
"Please make sure you compiled your code with '-parameters' option. " +
"If not, you need to specify parameter and header names with @" +
Param.class.getSimpleName() + " and @" + Header.class.getSimpleName() + '.');
"Alternatively, you could specify the name explicitly in the annotation.");
}
return parameter.getName();
}
Expand All @@ -100,7 +76,6 @@ static String getName(Object element) {
throw new IllegalArgumentException("cannot find the name: " + element.getClass().getName());
}

@VisibleForTesting
static String toHeaderName(String name) {
requireNonNull(name, "name");
checkArgument(!name.isEmpty(), "name is empty.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static com.linecorp.armeria.internal.server.annotation.AnnotatedElementNameUtil.findName;
import static com.linecorp.armeria.internal.server.annotation.AnnotatedElementNameUtil.getName;
import static com.linecorp.armeria.internal.server.annotation.AnnotatedElementNameUtil.getNameOrDefault;
import static com.linecorp.armeria.internal.server.annotation.AnnotatedElementNameUtil.toHeaderName;
import static com.linecorp.armeria.internal.server.annotation.AnnotatedServiceFactory.findDescription;
import static com.linecorp.armeria.internal.server.annotation.AnnotatedServiceTypeUtil.stringToType;
import static com.linecorp.armeria.internal.server.annotation.DefaultValues.getSpecifiedValue;
Expand Down Expand Up @@ -64,6 +65,7 @@
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Primitives;

import com.linecorp.armeria.common.AggregatedHttpRequest;
import com.linecorp.armeria.common.Cookie;
Expand All @@ -85,6 +87,7 @@
import com.linecorp.armeria.internal.server.annotation.AnnotatedBeanFactoryRegistry.BeanFactoryId;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.annotation.AnnotatedService;
import com.linecorp.armeria.server.annotation.Attribute;
import com.linecorp.armeria.server.annotation.ByteArrayRequestConverterFunction;
import com.linecorp.armeria.server.annotation.Default;
import com.linecorp.armeria.server.annotation.Delimiter;
Expand All @@ -100,6 +103,7 @@
import com.linecorp.armeria.server.docs.DescriptionInfo;

import io.netty.handler.codec.http.HttpConstants;
import io.netty.util.AttributeKey;
import scala.concurrent.ExecutionContext;

final class AnnotatedValueResolver {
Expand Down Expand Up @@ -445,9 +449,16 @@ private static AnnotatedValueResolver of(AnnotatedElement annotatedElement,
requireNonNull(dependencyInjector, "dependencyInjector");

final DescriptionInfo description = findDescription(annotatedElement);

final Attribute attr = annotatedElement.getAnnotation(Attribute.class);
if (attr != null) {
final String name = findName(typeElement, attr.value());
return ofAttribute(name, attr, annotatedElement, typeElement, type, description);
}

final Param param = annotatedElement.getAnnotation(Param.class);
if (param != null) {
final String name = findName(param, typeElement);
final String name = findName(typeElement, param.value());
if (type == File.class || type == Path.class || type == MultipartFile.class) {
return ofFileParam(name, annotatedElement, typeElement, type, description);
}
Expand All @@ -460,7 +471,7 @@ private static AnnotatedValueResolver of(AnnotatedElement annotatedElement,

final Header header = annotatedElement.getAnnotation(Header.class);
if (header != null) {
final String name = findName(header, typeElement);
final String name = toHeaderName(findName(typeElement, header.value()));
return ofHeader(name, annotatedElement, typeElement, type, description);
}

Expand Down Expand Up @@ -521,6 +532,7 @@ static List<RequestObjectResolver> addToFirstIfExists(List<RequestObjectResolver

private static boolean isAnnotationPresent(AnnotatedElement element) {
return element.isAnnotationPresent(Param.class) ||
element.isAnnotationPresent(Attribute.class) ||
element.isAnnotationPresent(Header.class) ||
element.isAnnotationPresent(RequestObject.class);
}
Expand Down Expand Up @@ -622,6 +634,34 @@ private static AnnotatedValueResolver ofRequestObject(String name, AnnotatedElem
.build();
}

private static AnnotatedValueResolver ofAttribute(String name,
Attribute attr,
AnnotatedElement annotatedElement,
AnnotatedElement typeElement, Class<?> type,
DescriptionInfo description) {

final ImmutableList.Builder<AttributeKey<?>> builder = ImmutableList.builder();

if (attr.prefix() != Attribute.class) {
builder.add(AttributeKey.valueOf(attr.prefix(), name));
} else {
final Class<?> serviceClass = ((Parameter) annotatedElement).getDeclaringExecutable()
.getDeclaringClass();
builder.add(AttributeKey.valueOf(serviceClass, name));
builder.add(AttributeKey.valueOf(name));
}

final ImmutableList<AttributeKey<?>> attrKeys = builder.build();
return new Builder(annotatedElement, type, name)
.annotationType(Attribute.class)
.typeElement(typeElement)
.supportDefault(true)
.supportContainer(true)
.description(description)
.resolver(attributeResolver(attrKeys))
.build();
}

@Nullable
private static AnnotatedValueResolver ofInjectableTypes(String name, AnnotatedElement annotatedElement,
Class<?> type, boolean useBlockingExecutor) {
Expand Down Expand Up @@ -819,6 +859,36 @@ private static AnnotatedValueResolver ofInjectableTypes0(String name, AnnotatedE
};
}

/**
* Returns an attribute resolver which retrieves a value specified by {@code attrKeys}
* from the {@link RequestContext}.
*/
private static BiFunction<AnnotatedValueResolver, ResolverContext, Object>
attributeResolver(Iterable<AttributeKey<?>> attrKeys) {
return (resolver, ctx) -> {
Class<?> targetType = resolver.rawType();
if (targetType.isPrimitive()) {
targetType = Primitives.wrap(targetType);
}
Comment on lines +870 to +872
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Micro optimization) Should we do this when rawType is created to avoid additional costs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right 👍

Using rawType might result in some additional costs due to Wrapping in all AnnotatedValueResolvers that do not use rawType. However, unnecessary calls will be made at most once during initialization, and afterwards, it could save on the cost of Wrapping with each incoming user request.


for (AttributeKey<?> attrKey : attrKeys) {
final Object value = ctx.context.attr(attrKey);
if (value != null) {
if (!targetType.isInstance(value)) {
throw new IllegalStateException(
String.format("Cannot inject the attribute '%s' due to " +
"mismatching type (expected: %s, actual: %s)",
attrKey.name(),
targetType.getName(),
value.getClass().getName()));
}
return value;
ikhoon marked this conversation as resolved.
Show resolved Hide resolved
}
}
return resolver.defaultOrException();
};
}

private static BiFunction<AnnotatedValueResolver, ResolverContext, Object> fileResolver() {
return (resolver, ctx) -> {
final FileAggregatedMultipart fileAggregatedMultipart = ctx.aggregatedMultipart();
Expand Down Expand Up @@ -897,6 +967,7 @@ static boolean isAnnotatedNullable(AnnotatedElement annotatedElement) {

@Nullable
private final Class<?> containerType;
private final Class<?> rawType;
private final Class<?> elementType;
@Nullable
private final ParameterizedType parameterizedElementType;
Expand All @@ -921,6 +992,7 @@ private AnnotatedValueResolver(@Nullable Class<? extends Annotation> annotationT
boolean isPathVariable, boolean shouldExist,
boolean shouldWrapValueAsOptional,
@Nullable Class<?> containerType, Class<?> elementType,
Class<?> rawType,
@Nullable ParameterizedType parameterizedElementType,
@Nullable String defaultValue,
DescriptionInfo description,
Expand All @@ -936,6 +1008,7 @@ private AnnotatedValueResolver(@Nullable Class<? extends Annotation> annotationT
this.parameterizedElementType = parameterizedElementType;
this.description = requireNonNull(description, "description");
this.containerType = containerType;
this.rawType = rawType;
this.resolver = requireNonNull(resolver, "resolver");
this.beanFactoryId = beanFactoryId;
this.aggregationStrategy = requireNonNull(aggregationStrategy, "aggregationStrategy");
Expand Down Expand Up @@ -981,6 +1054,10 @@ Class<?> containerType() {
return containerType;
}

Class<?> rawType() {
return rawType;
}

Class<?> elementType() {
return elementType;
}
Expand Down Expand Up @@ -1053,7 +1130,8 @@ private Object defaultOrException() {
}
return defaultValue;
}
throw new IllegalArgumentException("Mandatory parameter/header is missing: " + httpElementName);
throw new IllegalArgumentException("Mandatory parameter/header/attribute is missing: " +
httpElementName);
}

@Override
Expand Down Expand Up @@ -1107,6 +1185,7 @@ private Builder(AnnotatedElement annotatedElement, Type type, String name) {
*/
private Builder annotationType(Class<? extends Annotation> annotationType) {
assert annotationType == Param.class ||
annotationType == Attribute.class ||
annotationType == Header.class ||
annotationType == RequestObject.class : annotationType.getSimpleName();
this.annotationType = annotationType;
Expand Down Expand Up @@ -1241,6 +1320,13 @@ private AnnotatedValueResolver build() {
}

final Class<?> containerType = getContainerType(unwrappedParameterizedType);
final Class<?> rawType;
final Class<?> mayRawType = toRawType(unwrappedParameterizedType);
if (mayRawType.isPrimitive()) {
rawType = Primitives.wrap(mayRawType);
} else {
rawType = mayRawType;
}
final Class<?> elementType;
final ParameterizedType parameterizedElementType;

Expand Down Expand Up @@ -1269,7 +1355,7 @@ private AnnotatedValueResolver build() {
}

return new AnnotatedValueResolver(annotationType, httpElementName, pathVariable, shouldExist,
isOptional, containerType, elementType,
isOptional, containerType, elementType, rawType,
parameterizedElementType, defaultValue, description, resolver,
beanFactoryId, aggregation);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2016 LINE Corporation
*
* LINE Corporation 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:
*
* https://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 com.linecorp.armeria.server.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.linecorp.armeria.common.RequestContext;
import com.linecorp.armeria.common.annotation.UnstableApi;

import io.netty.util.AttributeKey;

/**
* Annotation for mapping an attribute of the given {@link AttributeKey}, retrieved
* from a {@link RequestContext}, onto the following elements.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@UnstableApi
public @interface Attribute {
chickenchickenlove marked this conversation as resolved.
Show resolved Hide resolved

/**
* The class of the {@link AttributeKey} to bind to. If you created an {@link AttributeKey} with
* {@code AttributeKey.valueOf(MyAttributeKeys.class, "INT_ATTR")},
* the {@link #prefix()} should be {@code MyAttributeKeys.class}.
* See <a href="https://armeria.dev/docs/advanced-custom-attributes/">advanced-customer-attributes</a>.
*/
Class<?> prefix() default Attribute.class;
trustin marked this conversation as resolved.
Show resolved Hide resolved

/**
* The name of the {@link AttributeKey} to bind to.
* You might also want to specify the {@link #prefix()}.
*/
String value();
}
Loading
Loading