Skip to content

Commit

Permalink
Add DependencyInjector to inject dependencies in annotations (#4202)
Browse files Browse the repository at this point in the history
Motivation:
Users cannot inject an instance, which has constructor parameters, through `@Decorator` and other annotations.
It would be nice if we provide a way to inject dependencies.

Modifications:
- Add `DependencyInjector` to inject the instances that are specified in an annotated service.
  - If a `DependencyInjector` is not set, `ReflectiveDependencyInjector` is used to keep the backward-compatibility
  - If a `DependencyInjector` is set, `BuiltInDependencyInjector` is applied at first to support built-in classes.
- Add `SpringDependencyInjector` that injects using `BeanFactory`. 
  - You can set `armeria.enable-auto-injection` to `true` to apply `SpringDependencyInjector`.
  - You can also create a bean for `DependencyInjector`.

Result:
- Close #4006
- You can now inject dependencies using `DependencyInjector` to annotated services.
  ```java
  // Inject authClient that is needed to create the AuthDecorator.
  WebClient authClient = ...
  DependencyInjector injector = DependencyInjector.ofSingletons(new AuthDecorator(authClient));
  serverBuilder.dependencyInjector(dependencyInjector, true);

  // An annotated service that uses AuthDecorator.
  @get("/foo")
  @decorator(AuthDecorator.class)
  public FooResponse foo(FooRequest req) {
      // Authrorized request.
      ...
  }
  
  // authClient is injected.
  class AuthDecorator implements DecoratingHttpServiceFunction {
      AuthDecorator(WebClient authClient) { ... }

      @OverRide
      public HttpResponse serve(HttpService delegate, ServiceRequestContext ctx, HttpRequest req)
              throws Exception {
          // Authorize the request.
          ...
      }
  }
  ```
  • Loading branch information
minwoox authored Jul 5, 2022
1 parent fdd767d commit 91c787f
Show file tree
Hide file tree
Showing 42 changed files with 1,314 additions and 246 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2022 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.common;

import static java.util.Objects.requireNonNull;

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.MoreObjects;

final class DefaultDependencyInjector implements DependencyInjector {

private static final Logger logger = LoggerFactory.getLogger(DefaultDependencyInjector.class);

private final Map<Class<?>, Object> singletons = new HashMap<>();
private boolean isShutdown;

DefaultDependencyInjector(Iterable<Object> singletons) {
requireNonNull(singletons, "singletons");
for (Object singleton : singletons) {
requireNonNull(singleton, "singleton");
this.singletons.put(singleton.getClass(), singleton);
}
}

@Override
public synchronized <T> T getInstance(Class<T> type) {
if (isShutdown) {
throw new IllegalStateException("Already shut down");
}
final Object instance = singletons.get(type);
if (instance != null) {
//noinspection unchecked
return (T) instance;
}
return null;
}

@Override
public synchronized void close() {
if (isShutdown) {
return;
}
isShutdown = true;
for (Object instance : singletons.values()) {
if (instance instanceof AutoCloseable) {
close((AutoCloseable) instance);
}
}
singletons.clear();
}

private static void close(AutoCloseable closeable) {
try {
closeable.close();
} catch (Exception e) {
logger.warn("Unexpected exception while closing {}", closeable, e);
}
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("singletons", singletons)
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2022 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.common;

import static java.util.Objects.requireNonNull;

import com.google.common.collect.ImmutableList;

import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.annotation.UnstableApi;
import com.linecorp.armeria.common.util.SafeCloseable;
import com.linecorp.armeria.server.Server;
import com.linecorp.armeria.server.annotation.Decorator;
import com.linecorp.armeria.server.annotation.DecoratorFactory;
import com.linecorp.armeria.server.annotation.ExceptionHandler;
import com.linecorp.armeria.server.annotation.RequestConverter;
import com.linecorp.armeria.server.annotation.ResponseConverter;

/**
* Injects dependencies that are specified in {@link RequestConverter#value()},
* {@link ResponseConverter#value()}, {@link ExceptionHandler#value()}, {@link Decorator#value()} and
* {@link DecoratorFactory#value()}.
* If the dependencies are not injected by this {@link DependencyInjector}, they are created via the default
* constructor, which does not have a parameter, of the classes.
*/
@UnstableApi
public interface DependencyInjector extends SafeCloseable {

/**
* Returns a {@link DependencyInjector} that injects dependencies using the specified singleton instances.
* The instances are {@linkplain AutoCloseable#close() closed} if it implements {@link AutoCloseable}
* when the {@link Server} stops.
*/
static DependencyInjector ofSingletons(Object... singletons) {
return ofSingletons(ImmutableList.copyOf(requireNonNull(singletons, "singletons")));
}

/**
* Returns a {@link DependencyInjector} that injects dependencies using the specified singleton instances.
* The instances are {@linkplain AutoCloseable#close() closed} if it implements {@link AutoCloseable}
* when the {@link Server} stops.
*/
static DependencyInjector ofSingletons(Iterable<Object> singletons) {
return new DefaultDependencyInjector(singletons);
}

/**
* Returns the instance of the specified {@link Class}.
*/
@Nullable
<T> T getInstance(Class<T> type);

/**
* Returns a new {@link DependencyInjector} that tries {@link #getInstance(Class)} of
* this {@link DependencyInjector} first and the specified {@link DependencyInjector}.
*/
default DependencyInjector orElse(DependencyInjector dependencyInjector) {
requireNonNull(dependencyInjector, "dependencyInjector");
return new OrElseDependencyInjector(this, dependencyInjector);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2022 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.common;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class OrElseDependencyInjector implements DependencyInjector {

private static final Logger logger = LoggerFactory.getLogger(OrElseDependencyInjector.class);

private final DependencyInjector first;
private final DependencyInjector second;

OrElseDependencyInjector(DependencyInjector first, DependencyInjector second) {
this.first = first;
this.second = second;
}

@Override
public <T> T getInstance(Class<T> type) {
final T instance = first.getInstance(type);
if (instance != null) {
return instance;
}
return second.getInstance(type);
}

@Override
public void close() {
close(first);
close(second);
}

private static void close(DependencyInjector dependencyInjector) {
try {
dependencyInjector.close();
} catch (Throwable t) {
logger.warn("Unexpected exception while closing {}", dependencyInjector, t);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2022 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.internal.common;

import static com.linecorp.armeria.internal.common.ReflectiveDependencyInjector.create;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import com.google.common.collect.ImmutableSet;

import com.linecorp.armeria.common.DependencyInjector;
import com.linecorp.armeria.server.annotation.ServerSentEventResponseConverterFunction;
import com.linecorp.armeria.server.annotation.decorator.LoggingDecoratorFactoryFunction;
import com.linecorp.armeria.server.annotation.decorator.RateLimitingDecoratorFactoryFunction;

public enum BuiltInDependencyInjector implements DependencyInjector {

INSTANCE;

//TODO(minwoox): Consider organizing built in class in a package and use reflection.
private static final Set<Class<?>> builtInClasses =
ImmutableSet.of(LoggingDecoratorFactoryFunction.class,
RateLimitingDecoratorFactoryFunction.class,
ServerSentEventResponseConverterFunction.class);

private static final Map<Class<?>, Object> instances = new ConcurrentHashMap<>();

@Override
public <T> T getInstance(Class<T> type) {
if (!builtInClasses.contains(type)) {
return null;
}

//noinspection unchecked
return (T) instances.computeIfAbsent(type, key -> {
final Object instance = create(key, null);
assert instance != null;
return instance;
});
}

@Override
public void close() {
// No need to close.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2022 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.internal.common;

import static org.reflections.ReflectionUtils.getConstructors;
import static org.reflections.ReflectionUtils.withParametersCount;

import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.MoreObjects;
import com.google.common.collect.Iterables;

import com.linecorp.armeria.common.DependencyInjector;
import com.linecorp.armeria.common.annotation.Nullable;

public final class ReflectiveDependencyInjector implements DependencyInjector {

private static final Logger logger = LoggerFactory.getLogger(ReflectiveDependencyInjector.class);

private final Map<Class<?>, Object> instances = new HashMap<>();

private boolean isShutdown;

@Override
public synchronized <T> T getInstance(Class<T> type) {
if (isShutdown) {
throw new IllegalStateException("Already shut down");
}
final Object instance = instances.get(type);
if (instance != null) {
//noinspection unchecked
return (T) instance;
}
return create(type, instances);
}

@Nullable
public static <T> T create(Class<? extends T> type, @Nullable Map<Class<?>, Object> instanceStorage) {
@SuppressWarnings("unchecked")
final Constructor<? extends T> constructor =
Iterables.getFirst(getConstructors(type, withParametersCount(0)), null);
if (constructor == null) {
return null;
}
constructor.setAccessible(true);
final T instance;
try {
instance = constructor.newInstance();
if (instanceStorage != null) {
instanceStorage.put(type, instance);
}
} catch (Throwable t) {
throw new IllegalArgumentException("cannot create an instance of " + type.getName(), t);
}
return instance;
}

@Override
public synchronized void close() {
if (isShutdown) {
return;
}
isShutdown = true;
for (Object instance : instances.values()) {
if (instance instanceof AutoCloseable) {
try {
((AutoCloseable) instance).close();
} catch (Exception e) {
logger.warn("Unexpected exception while closing {}", instance);
}
}
}
instances.clear();
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("instances", instances)
.toString();
}
}
Loading

0 comments on commit 91c787f

Please sign in to comment.