simplify scm-manager bootstrap

We have simplified the scm-manager bootstrap process, by replacing the
ServletContextListener call hierarchy with much simpler ModuleProviders.
We have also removed the parent injector. Now we create always a new
injector. If something goes wrong in the process of injector creation,
we will show a nicely styled error page instead of stacktrace on a white
page.
This commit is contained in:
Sebastian Sdorra
2019-06-24 16:59:28 +02:00
parent f0bb55e77b
commit 9662b8a00b
17 changed files with 670 additions and 312 deletions

View File

@@ -0,0 +1,256 @@
package sonia.scm.boot;
import com.google.common.base.Strings;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.multibindings.Multibinder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.CloseableModule;
import sonia.scm.Default;
import sonia.scm.EagerSingleton;
import sonia.scm.EagerSingletonModule;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class InjectionLifeCycleTest {
@Mock
private ServletContext servletContext;
@Test
void shouldInitializeEagerSingletons() {
Injector injector = initialize(new EagerSingletonModule(), new EagerModule());
Messenger messenger = injector.getInstance(Messenger.class);
assertThat(messenger.receive()).isEqualTo("eager baby!");
}
@Test
void shouldNotThrowAnExceptionWithoutEagerSingletons() {
Injector injector = initialize(new EagerSingletonModule());
Messenger messenger = injector.getInstance(Messenger.class);
assertThat(messenger.receive()).isNull();
}
@Test
void shouldInitializeServletContextListeners() {
Injector injector = initialize(new ServletContextListenerModule());
Messenger messenger = injector.getInstance(Messenger.class);
assertThat(messenger.receive()).isEqualTo("+4+2");
}
@Test
void shouldCallDestroyOnServletContextListeners() {
Injector injector = createInjector(servletContext, new ServletContextListenerModule());
InjectionLifeCycle lifeCycle = new InjectionLifeCycle(injector);
lifeCycle.shutdown();
Messenger messenger = injector.getInstance(Messenger.class);
assertThat(messenger.receive()).isEqualTo("-4-2");
}
@Test
void shouldCloseInstantiatedCloseables() {
Injector injector = createInjector(servletContext, new FortyTwoModule(), new CloseableModule());
injector.getInstance(Two.class);
injector.getInstance(Four.class);
InjectionLifeCycle lifeCycle = new InjectionLifeCycle(injector);
lifeCycle.shutdown();
Messenger messenger = injector.getInstance(Messenger.class);
assertThat(messenger.receive()).isEqualTo("42");
}
private Injector initialize(Module... modules) {
Injector injector = createInjector(servletContext, modules);
InjectionLifeCycle lifeCycle = new InjectionLifeCycle(injector);
lifeCycle.initialize();
return injector;
}
public static class EagerModule extends AbstractModule {
@Override
protected void configure() {
bind(ImEager.class);
}
}
@EagerSingleton
public static class ImEager {
@Inject
public ImEager(Messenger messenger) {
messenger.send("eager baby!");
}
}
public static class FortyTwoModule extends AbstractModule {
@Override
protected void configure() {
bind(Four.class);
bind(Two.class);
}
}
@Singleton
public static class Four implements Closeable {
private final Messenger messenger;
@Inject
public Four(Messenger messenger) {
this.messenger = messenger;
}
@Override
public void close() {
messenger.append("4");
}
}
@Singleton
public static class Two implements Closeable {
private final Messenger messenger;
@Inject
public Two(Messenger messenger) {
this.messenger = messenger;
}
@Override
public void close() {
messenger.append("2");
}
}
static Injector createInjector(ServletContext context, Module... modules) {
List<Module> moduleList = new ArrayList<>();
moduleList.add(new ServletContextModule(context));
moduleList.addAll(Arrays.asList(modules));
return Guice.createInjector(moduleList);
}
public static class ServletContextModule extends AbstractModule {
private final ServletContext servletContext;
ServletContextModule(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Override
protected void configure() {
bind(ServletContext.class).annotatedWith(Default.class).toInstance(servletContext);
}
}
public static class ServletContextListenerModule extends AbstractModule {
@Override
protected void configure() {
Multibinder<ServletContextListener> multibinder = Multibinder.newSetBinder(binder(), ServletContextListener.class);
multibinder.addBinding().to(AppendingFourServletContextListener.class);
multibinder.addBinding().to(AppendingTwoServletContextListener.class);
}
}
public static class AppendingFourServletContextListener extends AppendingServletContextListener {
@Inject
public AppendingFourServletContextListener(Messenger messenger) {
super(messenger);
}
@Override
protected String getSign() {
return "4";
}
}
public static class AppendingTwoServletContextListener extends AppendingServletContextListener {
@Inject
public AppendingTwoServletContextListener(Messenger messenger) {
super(messenger);
}
@Override
protected String getSign() {
return "2";
}
}
public static abstract class AppendingServletContextListener implements ServletContextListener {
private final Messenger messenger;
@Inject
public AppendingServletContextListener(Messenger messenger) {
this.messenger = messenger;
}
@Override
public void contextInitialized(ServletContextEvent sce) {
send("+");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
send("-");
}
private void send(String prefix) {
messenger.append(prefix + getSign());
}
protected abstract String getSign();
}
@Singleton
public static class Messenger {
private String message;
void send(String message) {
this.message = message;
}
void append(String messageToAppend) {
send(Strings.nullToEmpty(message) + messageToAppend);
}
String receive() {
return message;
}
}
}

View File

@@ -0,0 +1,64 @@
package sonia.scm.boot;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceFilter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.Default;
import javax.inject.Inject;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ServletContextModuleTest {
@Mock
private ServletContext servletContext;
private GuiceFilter guiceFilter;
@BeforeEach
void setUpEnvironment() throws ServletException {
guiceFilter = new GuiceFilter();
FilterConfig filterConfig = mock(FilterConfig.class);
when(filterConfig.getServletContext()).thenReturn(servletContext);
guiceFilter.init(filterConfig);
}
@AfterEach
void tearDownEnvironment() {
guiceFilter.destroy();
}
@Test
void shouldBeAbleToInjectServletContext() {
Injector injector = Guice.createInjector(new ServletContextModule());
WebComponent instance = injector.getInstance(WebComponent.class);
assertThat(instance.context).isSameAs(servletContext);
}
public static class WebComponent {
private ServletContext context;
@Inject
public WebComponent(@Default ServletContext context) {
this.context = context;
}
}
}

View File

@@ -1,6 +1,5 @@
package sonia.scm.boot;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;

View File

@@ -1,20 +1,17 @@
package sonia.scm.boot;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceFilter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@@ -26,22 +23,19 @@ import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class SingleViewTest {
@Mock
private ServletContext servletContext;
@Mock
private HttpServletRequest request;
@Captor
private ArgumentCaptor<Injector> captor;
private GuiceFilter guiceFilter;
@BeforeEach
void setUpGuiceFilter() throws ServletException {
guiceFilter = new GuiceFilter();
ServletContext servletContext = mock(ServletContext.class);
FilterConfig config = mock(FilterConfig.class);
doReturn(servletContext).when(config).getServletContext();
guiceFilter.init(config);
}
@@ -52,10 +46,10 @@ class SingleViewTest {
@Test
void shouldCreateViewControllerForView() {
ServletContextListener listener = SingleView.view("/my-template", 409);
ModuleProvider moduleProvider = SingleView.view("/my-template", 409);
when(request.getContextPath()).thenReturn("/scm");
ViewController instance = findViewController(listener);
ViewController instance = findViewController(moduleProvider);
assertThat(instance.getTemplate()).isEqualTo("/my-template");
View view = instance.createView(request);
@@ -64,17 +58,17 @@ class SingleViewTest {
@Test
void shouldCreateViewControllerForError() {
ServletContextListener listener = SingleView.error(new IOException("awesome io"));
ModuleProvider moduleProvider = SingleView.error(new IOException("awesome io"));
when(request.getContextPath()).thenReturn("/scm");
ViewController instance = findViewController(listener);
ViewController instance = findViewController(moduleProvider);
assertErrorViewController(instance, "awesome io");
}
@Test
void shouldBindServlets() {
ServletContextListener listener = SingleView.error(new IOException("awesome io"));
Injector injector = findInjector(listener);
ModuleProvider moduleProvider = SingleView.error(new IOException("awesome io"));
Injector injector = Guice.createInjector(moduleProvider.createModules());
assertThat(injector.getInstance(StaticResourceServlet.class)).isNotNull();
assertThat(injector.getInstance(SingleViewServlet.class)).isNotNull();
@@ -94,18 +88,9 @@ class SingleViewTest {
);
}
private ViewController findViewController(ServletContextListener listener) {
Injector injector = findInjector(listener);
private ViewController findViewController(ModuleProvider moduleProvider) {
Injector injector = Guice.createInjector(moduleProvider.createModules());
return injector.getInstance(ViewController.class);
}
private Injector findInjector(ServletContextListener listener) {
listener.contextInitialized(new ServletContextEvent(servletContext));
verify(servletContext).setAttribute(anyString(), captor.capture());
return captor.getValue();
}
}