Skip to content

Commit

Permalink
Add ArmeriaProcessor to provide ActionHook (#3732)
Browse files Browse the repository at this point in the history
Motivation:

No `ActionHook` is set because `TomcarService` makes a pure `Request`. Due to this, default error handling does not work when integrating with Spring Boot.

Modifications:

- Add `ArmeriaProcessor` per `tomcat` module
- Add `ArmeriaEndpoint` to `tomcat8`
- `ArmeriaProcessor` and `ArmeriaEndpoint` are created with `MethodHandle`
- Add test

Result:

- Closes #3447
  • Loading branch information
heowc committed Aug 18, 2021
1 parent be4250f commit 94e4c3f
Show file tree
Hide file tree
Showing 13 changed files with 707 additions and 27 deletions.
@@ -0,0 +1,72 @@
/*
* Copyright 2021 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.spring.tomcat.demo;

import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.google.common.collect.ImmutableMap;

@RestController
@RequestMapping("/error-handling")
public class ErrorHandlingController {

@GetMapping("/runtime-exception")
public void runtimeException() {
throw new RuntimeException("runtime exception");
}

@GetMapping("/custom-exception")
public void customException() {
throw new CustomException();
}

@GetMapping("/exception-handler")
public void exceptionHandler() {
throw new BaseException("exception handler");
}

@GetMapping("/global-exception-handler")
public void globalExceptionHandler() {
throw new GlobalBaseException("global exception handler");
}

@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "custom not found")
private static class CustomException extends RuntimeException {}

private static class BaseException extends RuntimeException {
BaseException(String message) {
super(message);
}
}

@ExceptionHandler(BaseException.class)
public ResponseEntity<Map<String, Object>> onBaseException(Throwable t) {
final Map<String, Object> body = ImmutableMap.<String, Object>builder()
.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value())
.put("message", t.getMessage())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2021 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.spring.tomcat.demo;

public class GlobalBaseException extends RuntimeException {
GlobalBaseException(String message) {
super(message);
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2021 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.spring.tomcat.demo;

import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.google.common.collect.ImmutableMap;

@RestControllerAdvice
class GlobalExceptionHandler {

@ExceptionHandler(GlobalBaseException.class)
public ResponseEntity<Map<String, Object>> onGlobalBaseException(Throwable t) {
final Map<String, Object> body = ImmutableMap.of("status", HttpStatus.INTERNAL_SERVER_ERROR.value(),
"message", t.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
Expand Up @@ -2,3 +2,6 @@ armeria:
ports:
- port: 0
protocol: HTTP
server:
error:
include-message: always
@@ -0,0 +1,63 @@
/*
* Copyright 2021 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.spring.tomcat.demo;

import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;

import javax.inject.Inject;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import com.linecorp.armeria.spring.LocalArmeriaPort;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ErrorHandlingTest {

private static final String TOMCAT_BASE_PATH = "/tomcat/api/rest/v1";

@LocalArmeriaPort
private int port;

@Inject
private TestRestTemplate restTemplate;

private static String tomcatBaseUrlPath(int port) {
return "http://localhost:" + port + TOMCAT_BASE_PATH;
}

@ParameterizedTest
@CsvSource({
"/error-handling/runtime-exception, 500, runtime exception",
"/error-handling/custom-exception, 404, custom not found",
"/error-handling/exception-handler, 500, exception handler",
"/error-handling/global-exception-handler, 500, global exception handler"
})
void shouldReturnFormattedMessage(String path, int status, String message) throws Exception {
final ResponseEntity<String> response =
restTemplate.getForEntity(tomcatBaseUrlPath(port) + path, String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.valueOf(status));
assertThatJson(response.getBody()).node("status").isEqualTo(status);
assertThatJson(response.getBody()).node("message").isEqualTo(message);
}
}
Expand Up @@ -20,9 +20,8 @@

import javax.inject.Inject;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
Expand All @@ -32,15 +31,13 @@
import org.springframework.boot.web.server.WebServer;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.junit4.SpringRunner;

import com.linecorp.armeria.internal.server.tomcat.TomcatVersion;
import com.linecorp.armeria.server.Server;
import com.linecorp.armeria.server.ServerPort;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SpringTomcatApplicationItTest {
class SpringTomcatApplicationItTest {
@Inject
private ApplicationContext applicationContext;
@Inject
Expand All @@ -55,7 +52,7 @@ public class SpringTomcatApplicationItTest {
@Value("${armeria-tomcat.version.minor:0}")
private int tomcatMinorVersion;

@Before
@BeforeEach
public void init() throws Exception {
httpPort = server.activePorts()
.values()
Expand All @@ -68,18 +65,18 @@ public void init() throws Exception {
}

@Test
public void contextLoads() {
void contextLoads() {
assertThat(greetingController).isNotNull();
}

@Test
public void verifyTomcatVersion() {
void verifyTomcatVersion() {
assertThat(TomcatVersion.major()).isEqualTo(tomcatMajorVersion);
assertThat(TomcatVersion.minor()).isEqualTo(tomcatMinorVersion);
}

@Test
public void verifySingleConnector() {
void verifySingleConnector() {
// Relevant to Tomcat 9.0
assertThat(applicationContext).isInstanceOf(WebServerApplicationContext.class);
final WebServer webServer = ((WebServerApplicationContext) applicationContext).getWebServer();
Expand All @@ -91,7 +88,7 @@ public void verifySingleConnector() {
}

@Test
public void greetingShouldReturnDefaultMessage() throws Exception {
void greetingShouldReturnDefaultMessage() throws Exception {
assertThat(restTemplate.getForObject("http://localhost:" +
httpPort +
"/tomcat/api/rest/v1/greeting",
Expand All @@ -100,7 +97,7 @@ public void greetingShouldReturnDefaultMessage() throws Exception {
}

@Test
public void greetingShouldReturnUsersMessage() throws Exception {
void greetingShouldReturnUsersMessage() throws Exception {
assertThat(restTemplate.getForObject("http://localhost:" +
httpPort +
"/tomcat/api/rest/v1/greeting?name=Armeria",
Expand All @@ -109,7 +106,7 @@ public void greetingShouldReturnUsersMessage() throws Exception {
}

@Test
public void greetingShouldReturn404() throws Exception {
void greetingShouldReturn404() throws Exception {
assertThat(restTemplate.getForEntity("http://localhost:" +
httpPort +
"/tomcat/api/rest/v1/greet",
Expand Down
25 changes: 15 additions & 10 deletions tomcat8/build.gradle
Expand Up @@ -24,13 +24,18 @@ dependencies {
// NB: We should never add these directories using the 'sourceSets' directive because that will make
// them added to more than one project and having a source directory with more than one output directory
// will confuse IDEs such as IntelliJ IDEA.
tasks.compileJava.source "${rootProject.projectDir}/tomcat9/src/main/java"
tasks.compileJava.exclude '**/ConfigFileLoaderInitializer*'
tasks.processResources.from "${rootProject.projectDir}/tomcat9/src/main/resources"
tasks.compileTestJava.source "${rootProject.projectDir}/tomcat9/src/test/java"
tasks.processTestResources.from "${rootProject.projectDir}/tomcat9/src/test/resources"
tasks.sourcesJar.from "${rootProject.projectDir}/tomcat9/src/main/java"
tasks.sourcesJar.from "${rootProject.projectDir}/tomcat9/src/main/resources"
tasks.sourcesJar.exclude '**/ConfigFileLoaderInitializer*'
tasks.javadoc.source "${rootProject.projectDir}/tomcat9/src/main/java"
tasks.javadoc.exclude '**/ConfigFileLoaderInitializer*'
def tomcat9ProjectDir = "${rootProject.projectDir}/tomcat9"
// Copy common files from tomcat9 module to gen-src directory in order to use them as a source set.
task generateSources(type: Copy) {
from "${tomcat9ProjectDir}/src/main/java"
into "${project.ext.genSrcDir}/main/java"
exclude '**/ArmeriaProcessor*'
exclude '**/ConfigFileLoaderInitializer*'
exclude '**/package-info.java'
}

tasks.compileJava.dependsOn(generateSources)
tasks.compileTestJava.source "${tomcat9ProjectDir}/src/test/java"
tasks.processResources.from "${tomcat9ProjectDir}/src/main/resources"
tasks.processTestResources.from "${tomcat9ProjectDir}/src/test/resources"
tasks.sourcesJar.from "${tomcat9ProjectDir}/src/main/resources"

0 comments on commit 94e4c3f

Please sign in to comment.