You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@shardingsphere.apache.org by te...@apache.org on 2020/08/20 15:14:32 UTC
[shardingsphere-elasticjob] branch restful-api updated: Add new
module elasticjob-restful (#1384)
This is an automated email from the ASF dual-hosted git repository.
technoboy pushed a commit to branch restful-api
in repository https://gitbox.apache.org/repos/asf/shardingsphere-elasticjob.git
The following commit(s) were added to refs/heads/restful-api by this push:
new 42a63e3 Add new module elasticjob-restful (#1384)
42a63e3 is described below
commit 42a63e302a1360e05dc5c15024cd1df74d0b1d04
Author: 吴伟杰 <ro...@me.com>
AuthorDate: Thu Aug 20 23:14:19 2020 +0800
Add new module elasticjob-restful (#1384)
---
elasticjob-infra/elasticjob-restful/README.md | 52 +++++++
elasticjob-infra/elasticjob-restful/pom.xml | 59 ++++++++
.../shardingsphere/elasticjob/restful/Http.java | 48 +++++++
.../elasticjob/restful/NettyRestfulService.java | 83 +++++++++++
.../restful/NettyRestfulServiceConfiguration.java | 68 +++++++++
.../elasticjob/restful/RestfulController.java | 24 ++++
.../elasticjob/restful/RestfulService.java | 34 +++++
.../elasticjob/restful/annotation/ContextPath.java | 37 +++++
.../elasticjob/restful/annotation/Mapping.java | 46 ++++++
.../elasticjob/restful/annotation/Param.java | 52 +++++++
.../elasticjob/restful/annotation/ParamSource.java | 50 +++++++
.../elasticjob/restful/annotation/RequestBody.java | 31 ++++
.../elasticjob/restful/annotation/Returning.java | 47 ++++++
.../deserializer/RequestBodyDeserializer.java | 41 ++++++
.../RequestBodyDeserializerFactory.java | 50 +++++++
.../impl/JsonRequestBodyDeserializer.java | 42 ++++++
.../impl/TextPlainRequestBodyDeserializer.java | 46 ++++++
.../restful/handler/ExceptionHandleResult.java | 32 +++++
.../restful/handler/ExceptionHandler.java | 35 +++++
.../elasticjob/restful/handler/HandleContext.java | 41 ++++++
.../elasticjob/restful/handler/Handler.java | 108 ++++++++++++++
.../restful/handler/HandlerMappingRegistry.java | 64 +++++++++
.../restful/handler/HandlerNotFoundException.java | 30 ++++
.../restful/handler/HandlerParameter.java | 38 +++++
.../mapping/AmbiguousPathPatternException.java | 28 ++++
.../restful/mapping/DefaultMappingContext.java | 43 ++++++
.../elasticjob/restful/mapping/MappingContext.java | 40 ++++++
.../elasticjob/restful/mapping/PathMatcher.java | 59 ++++++++
.../restful/mapping/RegexPathMatcher.java | 99 +++++++++++++
.../restful/mapping/RegexUrlPatternMap.java | 105 ++++++++++++++
.../elasticjob/restful/mapping/UrlPatternMap.java | 43 ++++++
.../restful/pipeline/ExceptionHandling.java | 102 +++++++++++++
.../restful/pipeline/HandleMethodExecutor.java | 76 ++++++++++
.../restful/pipeline/HandlerParameterDecoder.java | 157 +++++++++++++++++++++
.../restful/pipeline/HttpRequestDispatcher.java | 81 +++++++++++
.../pipeline/RestfulServiceChannelInitializer.java | 57 ++++++++
.../restful/serializer/ResponseBodySerializer.java | 39 +++++
.../serializer/ResponseBodySerializerFactory.java | 50 +++++++
.../impl/JsonResponseBodySerializer.java | 42 ++++++
...ob.restful.deserializer.RequestBodyDeserializer | 19 +++
...icjob.restful.serializer.ResponseBodySerializer | 18 +++
.../elasticjob/restful/RegexPathMatcherTest.java | 70 +++++++++
.../elasticjob/restful/RegexUrlPatternMapTest.java | 68 +++++++++
.../restful/controller/JobController.java | 90 ++++++++++++
.../CustomIllegalStateExceptionHandler.java | 33 +++++
.../elasticjob/restful/pipeline/HttpClient.java | 80 +++++++++++
.../pipeline/HttpRequestDispatcherTest.java | 38 +++++
.../restful/pipeline/NettyRestfulServiceTest.java | 122 ++++++++++++++++
.../elasticjob/restful/pojo/JobPojo.java | 37 +++++
.../elasticjob/restful/pojo/ResultDto.java | 30 ++++
elasticjob-infra/pom.xml | 1 +
51 files changed, 2785 insertions(+)
diff --git a/elasticjob-infra/elasticjob-restful/README.md b/elasticjob-infra/elasticjob-restful/README.md
new file mode 100644
index 0000000..c203953
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/README.md
@@ -0,0 +1,52 @@
+# Restful Service
+
+## Usage
+
+### Create a RestfulController
+```java
+@ContextPath("/job")
+public class JobController implements RestfulController {
+
+ @Mapping(method = Http.POST, pattern = "/{group}/{jobName}")
+ public JobPojo createJob(@Param(name = "group", source = ParamSource.PATH) final String group,
+ @Param(name = "jobName", source = ParamSource.PATH) final String jobName,
+ @Param(name = "cron", source = ParamSource.QUERY) final String cron,
+ @RequestBody String description) {
+ JobPojo jobPojo = new JobPojo();
+ jobPojo.setName(jobName);
+ jobPojo.setCron(cron);
+ jobPojo.setGroup(group);
+ jobPojo.setDescription(description);
+ return jobPojo;
+ }
+
+ @Mapping(method = Http.GET, pattern = "/code/204")
+ @Returning(code = 204)
+ public Object return204() {
+ return null;
+ }
+}
+```
+
+### (Optional) Create ExceptionHandler
+```java
+public class CustomIllegalStateExceptionHandler implements ExceptionHandler<IllegalStateException> {
+ @Override
+ public ExceptionHandleResult handleException(final IllegalStateException ex) {
+ return ExceptionHandleResult.builder()
+ .statusCode(403)
+ .contentType(Http.DEFAULT_CONTENT_TYPE)
+ .result(ResultDto.builder().code(1).data(ex.getLocalizedMessage()).build())
+ .build();
+ }
+}
+```
+
+### Configure Restful Service and Start Up
+```java
+NettyRestfulServiceConfiguration configuration = new NettyRestfulServiceConfiguration(8080);
+configuration.addControllerInstance(new JobController());
+configuration.addExceptionHandler(IllegalStateException.class, new CustomIllegalStateExceptionHandler());
+RestfulService restfulService = new NettyRestfulService(configuration);
+restfulService.startup();
+```
diff --git a/elasticjob-infra/elasticjob-restful/pom.xml b/elasticjob-infra/elasticjob-restful/pom.xml
new file mode 100644
index 0000000..fad0128
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/pom.xml
@@ -0,0 +1,59 @@
+<?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">
+ <parent>
+ <artifactId>elasticjob-infra</artifactId>
+ <groupId>org.apache.shardingsphere.elasticjob</groupId>
+ <version>3.0.0-beta-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>elasticjob-restful</artifactId>
+ <name>${project.artifactId}</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.shardingsphere.elasticjob</groupId>
+ <artifactId>elasticjob-infra-common</artifactId>
+ <version>${project.parent.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-codec-http</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-jdk14</artifactId>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/Http.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/Http.java
new file mode 100644
index 0000000..1d7fc47
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/Http.java
@@ -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.shardingsphere.elasticjob.restful;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+/**
+ * Constants for HTTP.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class Http {
+
+ public static final String GET = "GET";
+
+ public static final String POST = "POST";
+
+ public static final String HEAD = "HEAD";
+
+ public static final String PUT = "PUT";
+
+ public static final String PATCH = "PATCH";
+
+ public static final String OPTIONS = "OPTIONS";
+
+ public static final String DELETE = "DELETE";
+
+ public static final String TRACE = "TRACE";
+
+ public static final String CONNECT = "CONNECT";
+
+ public static final String DEFAULT_CONTENT_TYPE = "application/json; charset=utf-8";
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulService.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulService.java
new file mode 100644
index 0000000..6ea0362
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulService.java
@@ -0,0 +1,83 @@
+/*
+ * 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.shardingsphere.elasticjob.restful;
+
+import com.google.common.base.Strings;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.util.NettyRuntime;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.shardingsphere.elasticjob.restful.pipeline.RestfulServiceChannelInitializer;
+
+/**
+ * Implemented {@link RestfulService} via Netty.
+ */
+@Slf4j
+public final class NettyRestfulService implements RestfulService {
+
+ private static final int DEFAULT_WORKER_GROUP_THREADS = 1 + 2 * NettyRuntime.availableProcessors();
+
+ private final NettyRestfulServiceConfiguration configuration;
+
+ private ServerBootstrap serverBootstrap;
+
+ private EventLoopGroup bossEventLoopGroup;
+
+ private EventLoopGroup workerEventLoopGroup;
+
+ public NettyRestfulService(final NettyRestfulServiceConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ private void initServerBootstrap() {
+ bossEventLoopGroup = new NioEventLoopGroup();
+ workerEventLoopGroup = new NioEventLoopGroup(DEFAULT_WORKER_GROUP_THREADS);
+
+ serverBootstrap = new ServerBootstrap()
+ .group(bossEventLoopGroup, workerEventLoopGroup)
+ .channel(NioServerSocketChannel.class)
+ .childHandler(new RestfulServiceChannelInitializer(configuration));
+ }
+
+ @Override
+ public void startup() {
+ initServerBootstrap();
+ ChannelFuture channelFuture;
+ if (!Strings.isNullOrEmpty(configuration.getHost())) {
+ channelFuture = serverBootstrap.bind(configuration.getHost(), configuration.getPort());
+ } else {
+ channelFuture = serverBootstrap.bind(configuration.getPort());
+ }
+ channelFuture.addListener(future -> {
+ if (future.isSuccess()) {
+ log.info("Restful Service started on port {}.", configuration.getPort());
+ } else {
+ log.error("Failed to start Restful Service.", future.cause());
+ }
+ });
+ }
+
+ @Override
+ public void shutdown() {
+ bossEventLoopGroup.shutdownGracefully();
+ workerEventLoopGroup.shutdownGracefully();
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulServiceConfiguration.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulServiceConfiguration.java
new file mode 100644
index 0000000..d40bfcc
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulServiceConfiguration.java
@@ -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.shardingsphere.elasticjob.restful;
+
+import com.google.common.base.Preconditions;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import org.apache.shardingsphere.elasticjob.restful.handler.ExceptionHandler;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Configuration for {@link NettyRestfulService}.
+ */
+@Getter
+@RequiredArgsConstructor
+public final class NettyRestfulServiceConfiguration {
+
+ private final int port;
+
+ @Setter
+ private String host;
+
+ private final List<RestfulController> controllerInstances = new ArrayList<>();
+
+ private final Map<Class<? extends Throwable>, ExceptionHandler<? extends Throwable>> exceptionHandlers = new HashMap<>();
+
+ /**
+ * Add instances of RestfulController.
+ *
+ * @param instances instances of RestfulController
+ */
+ public void addControllerInstance(final RestfulController... instances) {
+ controllerInstances.addAll(Arrays.asList(instances));
+ }
+
+ /**
+ * Add an instance of ExceptionHandler for specific exception.
+ *
+ * @param exceptionType The type of exception to handle
+ * @param exceptionHandler Instance of ExceptionHandler
+ * @param <E> The type of exception to handle
+ */
+ public <E extends Throwable> void addExceptionHandler(final Class<E> exceptionType, final ExceptionHandler<E> exceptionHandler) {
+ Preconditions.checkState(!exceptionHandlers.containsKey(exceptionType), "ExceptionHandler for %s has already existed.", exceptionType.getName());
+ exceptionHandlers.put(exceptionType, exceptionHandler);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/RestfulController.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/RestfulController.java
new file mode 100644
index 0000000..a12688f
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/RestfulController.java
@@ -0,0 +1,24 @@
+/*
+ * 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.shardingsphere.elasticjob.restful;
+
+/**
+ * Mark a class as RestfulController.
+ */
+public interface RestfulController {
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/RestfulService.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/RestfulService.java
new file mode 100644
index 0000000..2abb8bf
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/RestfulService.java
@@ -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.shardingsphere.elasticjob.restful;
+
+/**
+ * A facade of restful service. Invoke startup() method to start listen a port to provide Restful API.
+ */
+public interface RestfulService {
+
+ /**
+ * Start Restful Service.
+ */
+ void startup();
+
+ /**
+ * Shutdown Restful Service.
+ */
+ void shutdown();
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/ContextPath.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/ContextPath.java
new file mode 100644
index 0000000..8972f72
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/ContextPath.java
@@ -0,0 +1,37 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ContextPath {
+
+ /**
+ * Context path.
+ * Starts with '/' and no '/' at the end.
+ * Such as <code>/api/app</code>.
+ *
+ * @return Context path
+ */
+ String value();
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/Mapping.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/Mapping.java
new file mode 100644
index 0000000..ee23a16
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/Mapping.java
@@ -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.shardingsphere.elasticjob.restful.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Declare what HTTP method and path is used to invoke the handler.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Mapping {
+
+ /**
+ * Http method.
+ *
+ * @return Http method
+ */
+ String method();
+
+ /**
+ * Path pattern of this handler.
+ * Such as <code>/app/{jobName}/enable</code>.
+ *
+ * @return Path pattern
+ */
+ String path() default "";
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/Param.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/Param.java
new file mode 100644
index 0000000..3e50641
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/Param.java
@@ -0,0 +1,52 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Specify name and source of the parameter.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface Param {
+
+ /**
+ * Parameter name.
+ *
+ * @return Parameter name
+ */
+ String name();
+
+ /**
+ * Source of parameter.
+ *
+ * @return Source of parameter
+ */
+ ParamSource source();
+
+ /**
+ * If the parameter is required.
+ *
+ * @return Requirement
+ */
+ boolean required() default true;
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/ParamSource.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/ParamSource.java
new file mode 100644
index 0000000..0eff5f0
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/ParamSource.java
@@ -0,0 +1,50 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.annotation;
+
+/**
+ * Sources of parameter.
+ *
+ * @see org.apache.shardingsphere.elasticjob.restful.annotation.Param
+ */
+public enum ParamSource {
+ /**
+ * Request path.
+ */
+ PATH,
+
+ /**
+ * Query parameters.
+ */
+ QUERY,
+
+ /**
+ * HTTP headers.
+ */
+ HEADER,
+
+ /**
+ * HTTP request body.
+ */
+ BODY,
+
+ /**
+ * Unknown source.
+ */
+ UNKNOWN,
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/RequestBody.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/RequestBody.java
new file mode 100644
index 0000000..20b41f5
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/RequestBody.java
@@ -0,0 +1,31 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotate the parameter which is from HTTP request body.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface RequestBody {
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/Returning.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/Returning.java
new file mode 100644
index 0000000..c628184
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/annotation/Returning.java
@@ -0,0 +1,47 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.annotation;
+
+import org.apache.shardingsphere.elasticjob.restful.Http;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotate on handler method to declare HTTP status code and content type of HTTP response.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Returning {
+
+ /**
+ * HTTP status code to return after handling.
+ *
+ * @return Http status code
+ */
+ int code() default 200;
+
+ /**
+ * HTTP content type of response.
+ *
+ * @return HTTP content type
+ */
+ String contentType() default Http.DEFAULT_CONTENT_TYPE;
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/RequestBodyDeserializer.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/RequestBodyDeserializer.java
new file mode 100644
index 0000000..9028045
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/RequestBodyDeserializer.java
@@ -0,0 +1,41 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.deserializer;
+
+/**
+ * Deserializer for deserializing request body with specific MIME type.
+ */
+public interface RequestBodyDeserializer {
+
+ /**
+ * Specify which type would be deserialized by this deserializer.
+ *
+ * @return MIME type
+ */
+ String mimeType();
+
+ /**
+ * Deserialize request body to an object.
+ *
+ * @param targetType Target type
+ * @param requestBodyBytes Request body bytes
+ * @param <T> Target type
+ * @return Deserialized object
+ */
+ <T> T deserialize(Class<T> targetType, byte[] requestBodyBytes);
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/RequestBodyDeserializerFactory.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/RequestBodyDeserializerFactory.java
new file mode 100644
index 0000000..7dc6ada
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/RequestBodyDeserializerFactory.java
@@ -0,0 +1,50 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.deserializer;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Request body deserializer factory.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class RequestBodyDeserializerFactory {
+
+ private static final Map<String, RequestBodyDeserializer> REQUEST_BODY_DESERIALIZERS = new ConcurrentHashMap<>();
+
+ static {
+ for (RequestBodyDeserializer deserializer : ServiceLoader.load(RequestBodyDeserializer.class)) {
+ REQUEST_BODY_DESERIALIZERS.put(deserializer.mimeType(), deserializer);
+ }
+ }
+
+ /**
+ * Get deserializer for specific HTTP content type.
+ *
+ * @param contentType HTTP content type
+ * @return Deserializer
+ */
+ public static RequestBodyDeserializer getRequestBodyDeserializer(final String contentType) {
+ return REQUEST_BODY_DESERIALIZERS.get(contentType);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/impl/JsonRequestBodyDeserializer.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/impl/JsonRequestBodyDeserializer.java
new file mode 100644
index 0000000..e742a6f
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/impl/JsonRequestBodyDeserializer.java
@@ -0,0 +1,42 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.deserializer.impl;
+
+import com.google.gson.Gson;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import org.apache.shardingsphere.elasticjob.restful.deserializer.RequestBodyDeserializer;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Deserializer for <code>application/json</code>.
+ */
+public final class JsonRequestBodyDeserializer implements RequestBodyDeserializer {
+
+ private final Gson gson = new Gson();
+
+ @Override
+ public String mimeType() {
+ return HttpHeaderValues.APPLICATION_JSON.toString();
+ }
+
+ @Override
+ public <T> T deserialize(final Class<T> targetType, final byte[] requestBodyBytes) {
+ return gson.fromJson(new String(requestBodyBytes, StandardCharsets.UTF_8), targetType);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/impl/TextPlainRequestBodyDeserializer.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/impl/TextPlainRequestBodyDeserializer.java
new file mode 100644
index 0000000..3268f0c
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/deserializer/impl/TextPlainRequestBodyDeserializer.java
@@ -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.shardingsphere.elasticjob.restful.deserializer.impl;
+
+import io.netty.handler.codec.http.HttpHeaderValues;
+import org.apache.shardingsphere.elasticjob.restful.deserializer.RequestBodyDeserializer;
+
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+
+/**
+ * Deserializer for <code>text/plain</code>.
+ */
+public final class TextPlainRequestBodyDeserializer implements RequestBodyDeserializer {
+
+ @Override
+ public String mimeType() {
+ return HttpHeaderValues.TEXT_PLAIN.toString();
+ }
+
+ @Override
+ public <T> T deserialize(final Class<T> targetType, final byte[] requestBodyBytes) {
+ if (byte[].class.equals(targetType)) {
+ return (T) requestBodyBytes;
+ }
+ if (String.class.isAssignableFrom(targetType)) {
+ return (T) new String(requestBodyBytes, StandardCharsets.UTF_8);
+ }
+ throw new UnsupportedOperationException(MessageFormat.format("Cannot deserialize [{0}] into [{1}]", mimeType(), targetType.getName()));
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/ExceptionHandleResult.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/ExceptionHandleResult.java
new file mode 100644
index 0000000..722d3a8
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/ExceptionHandleResult.java
@@ -0,0 +1,32 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.handler;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@Getter
+public final class ExceptionHandleResult {
+
+ private final Object result;
+
+ private final int statusCode;
+
+ private final String contentType;
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/ExceptionHandler.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/ExceptionHandler.java
new file mode 100644
index 0000000..3603046
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/ExceptionHandler.java
@@ -0,0 +1,35 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.handler;
+
+/**
+ * If an exception was thrown, {@link org.apache.shardingsphere.elasticjob.restful.pipeline.ExceptionHandling}
+ * will search a proper handler to handle it.
+ *
+ * @param <E> Type of Exception
+ */
+public interface ExceptionHandler<E extends Throwable> {
+
+ /**
+ * Handler for specific Exception.
+ *
+ * @param ex Exception
+ * @return Handle result
+ */
+ ExceptionHandleResult handleException(E ex);
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandleContext.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandleContext.java
new file mode 100644
index 0000000..d2adee5
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandleContext.java
@@ -0,0 +1,41 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.handler;
+
+import io.netty.handler.codec.http.FullHttpRequest;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import org.apache.shardingsphere.elasticjob.restful.mapping.MappingContext;
+
+/**
+ * HandleContext will hold a instance of HTTP request, {@link MappingContext} and arguments for handle method invoking.
+ *
+ * @param <T> Type of MappingContext
+ */
+@RequiredArgsConstructor
+@Getter
+@Setter
+public final class HandleContext<T> {
+
+ private final FullHttpRequest httpRequest;
+
+ private final MappingContext<T> mappingContext;
+
+ private Object[] args = new Object[0];
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/Handler.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/Handler.java
new file mode 100644
index 0000000..790e596
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/Handler.java
@@ -0,0 +1,108 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.handler;
+
+import lombok.Getter;
+import org.apache.shardingsphere.elasticjob.restful.Http;
+import org.apache.shardingsphere.elasticjob.restful.annotation.ParamSource;
+import org.apache.shardingsphere.elasticjob.restful.annotation.Param;
+import org.apache.shardingsphere.elasticjob.restful.annotation.RequestBody;
+import org.apache.shardingsphere.elasticjob.restful.annotation.Returning;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Handle holds a handle method and an instance for method invoking.
+ * Describes parameters requirements of handle method.
+ */
+public final class Handler {
+
+ private final Object instance;
+
+ private final Method handleMethod;
+
+ @Getter
+ private final List<HandlerParameter> handlerParameters;
+
+ /**
+ * HTTP status code to return.
+ */
+ @Getter
+ private final int httpStatusCode;
+
+ /**
+ * Content type to producing.
+ */
+ @Getter
+ private final String producing;
+
+ public Handler(final Object instance, final Method handleMethod) {
+ this.instance = instance;
+ this.handleMethod = handleMethod;
+ this.handlerParameters = parseHandleMethodParameter();
+ this.httpStatusCode = parseReturning();
+ this.producing = parseProducing();
+ }
+
+ /**
+ * Execute handle method with required arguments.
+ *
+ * @param args Required arguments
+ * @return Method invoke result
+ * @throws InvocationTargetException Wraps exception thrown by invoked method
+ * @throws IllegalAccessException Handle method is not accessible
+ */
+ public Object execute(final Object... args) throws InvocationTargetException, IllegalAccessException {
+ return handleMethod.invoke(instance, args);
+ }
+
+ private List<HandlerParameter> parseHandleMethodParameter() {
+ List<HandlerParameter> params = new LinkedList<>();
+ Parameter[] parameters = handleMethod.getParameters();
+ for (int i = 0; i < parameters.length; i++) {
+ Parameter parameter = parameters[i];
+ Param annotation = parameter.getAnnotation(Param.class);
+ HandlerParameter handlerParameter;
+ if (null != annotation) {
+ handlerParameter = new HandlerParameter(i, parameter.getType(), annotation.source(), annotation.name());
+ } else if (null != parameter.getAnnotation(RequestBody.class)) {
+ handlerParameter = new HandlerParameter(i, parameter.getType(), ParamSource.BODY, parameter.getName());
+ } else {
+ handlerParameter = new HandlerParameter(i, parameter.getType(), ParamSource.UNKNOWN, parameter.getName());
+ }
+ params.add(handlerParameter);
+ }
+ return Collections.unmodifiableList(params);
+ }
+
+ private int parseReturning() {
+ Returning returning = handleMethod.getAnnotation(Returning.class);
+ return Optional.ofNullable(returning).map(Returning::code).orElse(200);
+ }
+
+ private String parseProducing() {
+ Returning returning = handleMethod.getAnnotation(Returning.class);
+ return Optional.ofNullable(returning).map(Returning::contentType).orElse(Http.DEFAULT_CONTENT_TYPE);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandlerMappingRegistry.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandlerMappingRegistry.java
new file mode 100644
index 0000000..33b001e
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandlerMappingRegistry.java
@@ -0,0 +1,64 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.handler;
+
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequest;
+import org.apache.shardingsphere.elasticjob.restful.mapping.MappingContext;
+import org.apache.shardingsphere.elasticjob.restful.mapping.RegexUrlPatternMap;
+import org.apache.shardingsphere.elasticjob.restful.mapping.UrlPatternMap;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * HandlerMappingRegistry stores mappings of handlers.
+ * Search a proper {@link MappingContext} by HTTP method and request URI.
+ */
+public final class HandlerMappingRegistry {
+
+ private final Map<HttpMethod, UrlPatternMap<Handler>> mappings = new HashMap<>();
+
+ /**
+ * Get a MappingContext with Handler for the request.
+ *
+ * @param httpRequest Http request
+ * @return A MappingContext if matched. Return null if mismatched.
+ */
+ public MappingContext<Handler> getMappingContext(final HttpRequest httpRequest) {
+ UrlPatternMap<Handler> urlPatternMap = mappings.get(httpRequest.method());
+ String uriWithoutQuery = httpRequest.uri().split("\\?")[0];
+ return Optional
+ .ofNullable(urlPatternMap.match(uriWithoutQuery))
+ .orElse(null);
+ }
+
+ /**
+ * Add a Handler for a path pattern.
+ *
+ * @param method HTTP method
+ * @param pathPattern Path pattern
+ * @param handler Handler
+ */
+ public void addMapping(final HttpMethod method, final String pathPattern, final Handler handler) {
+ mappings.computeIfAbsent(method, httpMethod -> new RegexUrlPatternMap<>());
+ UrlPatternMap<Handler> urlPatternMap = mappings.get(method);
+ urlPatternMap.put(pathPattern, handler);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandlerNotFoundException.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandlerNotFoundException.java
new file mode 100644
index 0000000..f8f45a9
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandlerNotFoundException.java
@@ -0,0 +1,30 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.handler;
+
+import java.text.MessageFormat;
+
+public final class HandlerNotFoundException extends RuntimeException {
+
+ private final String path;
+
+ public HandlerNotFoundException(final String path) {
+ super(MessageFormat.format("No handler found for [{0}].", path));
+ this.path = path;
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandlerParameter.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandlerParameter.java
new file mode 100644
index 0000000..8a08ecc
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/handler/HandlerParameter.java
@@ -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.shardingsphere.elasticjob.restful.handler;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.apache.shardingsphere.elasticjob.restful.annotation.ParamSource;
+
+/**
+ * Describe parameters of a handle method.
+ */
+@RequiredArgsConstructor
+@Getter
+public final class HandlerParameter {
+
+ private final int index;
+
+ private final Class<?> type;
+
+ private final ParamSource paramSource;
+
+ private final String name;
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/AmbiguousPathPatternException.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/AmbiguousPathPatternException.java
new file mode 100644
index 0000000..0567184
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/AmbiguousPathPatternException.java
@@ -0,0 +1,28 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.mapping;
+
+/**
+ * A path matched more than one path patterns and failed to determine which pattern is more proper.
+ */
+public final class AmbiguousPathPatternException extends RuntimeException {
+
+ public AmbiguousPathPatternException(final String message) {
+ super(message);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/DefaultMappingContext.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/DefaultMappingContext.java
new file mode 100644
index 0000000..cac664e
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/DefaultMappingContext.java
@@ -0,0 +1,43 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.mapping;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Default mapping context.
+ *
+ * @param <T> Type of payload
+ */
+@RequiredArgsConstructor
+public final class DefaultMappingContext<T> implements MappingContext<T> {
+
+ private final String pattern;
+
+ private final T payload;
+
+ @Override
+ public String pattern() {
+ return pattern;
+ }
+
+ @Override
+ public T payload() {
+ return payload;
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/MappingContext.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/MappingContext.java
new file mode 100644
index 0000000..4b4e2b5
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/MappingContext.java
@@ -0,0 +1,40 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.mapping;
+
+/**
+ * MappingContext will hold a path pattern and a payload.
+ *
+ * @param <T> Payload type
+ */
+public interface MappingContext<T> {
+
+ /**
+ * The path pattern of this MappingContext.
+ *
+ * @return Path pattern
+ */
+ String pattern();
+
+ /**
+ * Payload of this MappingContext.
+ *
+ * @return Payload
+ */
+ T payload();
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/PathMatcher.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/PathMatcher.java
new file mode 100644
index 0000000..7588d27
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/PathMatcher.java
@@ -0,0 +1,59 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.mapping;
+
+import java.util.Map;
+
+/**
+ * PathMatcher is a supporting tool for HTTP request dispatching.
+ * <p>
+ * Used by {@link UrlPatternMap}, {@link org.apache.shardingsphere.elasticjob.restful.pipeline.HandlerParameterDecoder}
+ * for template variables extracting, path pattern validating, pattern matching.
+ * </p>
+ *
+ * @see RegexPathMatcher
+ */
+public interface PathMatcher {
+
+ /**
+ * Capture actual values of placeholder.
+ * The format of Path pattern likes <code>/app/{jobName}/{status}</code>.
+ *
+ * @param pathPattern Path pattern contains templates
+ * @param path Actual path
+ * @return Map from template name to actual value
+ */
+ Map<String, String> captureVariables(String pathPattern, String path);
+
+ /**
+ * Check if the path pattern matches the given path.
+ *
+ * @param pathPattern Path pattern
+ * @param path The path to check
+ * @return true if matched, or else false
+ */
+ boolean matches(String pathPattern, String path);
+
+ /**
+ * Check if the given string is a valid path pattern.
+ *
+ * @param pathPattern Path pattern to check
+ * @return true if valid, or else false
+ */
+ boolean isValidPathPattern(String pathPattern);
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/RegexPathMatcher.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/RegexPathMatcher.java
new file mode 100644
index 0000000..55e29a2
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/RegexPathMatcher.java
@@ -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.shardingsphere.elasticjob.restful.mapping;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Implemented {@link PathMatcher} by regular expression.
+ */
+public final class RegexPathMatcher implements PathMatcher {
+
+ private static final String PATH_SEPARATOR = "/";
+
+ private static final Pattern PATH_PATTERN = Pattern.compile("^(?:/|(/[^/{}?]+|/\\{[^/{}?]+})+)$");
+
+ private static final Pattern TEMPLATE_PATTERN = Pattern.compile("\\{(?<template>[^/]+)}");
+
+ private static final String TEMPLATE_REGEX = "(?<${template}>[^/]+)";
+
+ private final Map<String, Pattern> patternCache = new ConcurrentHashMap<>();
+
+ @Override
+ public Map<String, String> captureVariables(final String pathPattern, final String path) {
+ Pattern compiled = getCompiledPattern(pathPattern);
+ String pathWithoutQuery = trimUriQuery(path);
+ Matcher matcher = compiled.matcher(pathWithoutQuery);
+ if (!matcher.matches() || 0 == matcher.groupCount()) {
+ return Collections.emptyMap();
+ }
+ Map<String, String> variables = new LinkedHashMap<>();
+ for (String variableName : extractTemplateNames(pathPattern)) {
+ variables.put(variableName, matcher.group(variableName));
+ }
+ return Collections.unmodifiableMap(variables);
+ }
+
+ @Override
+ public boolean matches(final String pathPattern, final String path) {
+ return getCompiledPattern(pathPattern).matcher(trimUriQuery(path)).matches();
+ }
+
+ @Override
+ public boolean isValidPathPattern(final String pathPattern) {
+ return PATH_PATTERN.matcher(pathPattern).matches();
+ }
+
+ private Pattern getCompiledPattern(final String pathPattern) {
+ String regexPattern = convertToRegexPattern(pathPattern);
+ patternCache.computeIfAbsent(regexPattern, Pattern::compile);
+ return patternCache.get(regexPattern);
+ }
+
+ private String convertToRegexPattern(final String pathPattern) {
+ return TEMPLATE_PATTERN.matcher(pathPattern).replaceAll(TEMPLATE_REGEX);
+ }
+
+ private List<String> extractTemplateNames(final String pathPattern) {
+ String[] pathFragments = pathPattern.split(PATH_SEPARATOR);
+ List<String> names = new ArrayList<>();
+ for (String fragment : pathFragments) {
+ int start = fragment.indexOf('{');
+ int end = fragment.lastIndexOf('}');
+ if (-1 != start && -1 != end) {
+ names.add(fragment.substring(start + 1, end));
+ }
+ }
+ return names;
+ }
+
+ private String trimUriQuery(final String uri) {
+ int index = uri.indexOf('?');
+ if (-1 != index) {
+ return uri.substring(0, index);
+ }
+ return uri;
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/RegexUrlPatternMap.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/RegexUrlPatternMap.java
new file mode 100644
index 0000000..2b25777
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/RegexUrlPatternMap.java
@@ -0,0 +1,105 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.mapping;
+
+import com.google.common.base.Preconditions;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * Implemented {@link UrlPatternMap} by regular expression.
+ *
+ * @param <V> Type of payload
+ */
+public final class RegexUrlPatternMap<V> implements UrlPatternMap<V> {
+
+ private static final String PATH_SEPARATOR = "/";
+
+ private static final Pattern TEMPLATE_PATTERN = Pattern.compile("(?<=/)\\{(?<template>[^/]+)}");
+
+ private final Map<String, MappingContext<V>> map = new LinkedHashMap<>();
+
+ private final PathMatcher pathMatcher = new RegexPathMatcher();
+
+ @Override
+ public void put(final String pathPattern, final V value) {
+ Objects.requireNonNull(pathPattern, "Path pattern must be not null.");
+ Preconditions.checkArgument(pathMatcher.isValidPathPattern(pathPattern), "Path pattern [%s] invalid.", pathPattern);
+ String unified = unifyPattern(pathPattern);
+ MappingContext<V> mappingContext = new DefaultMappingContext<>(pathPattern, value);
+ if (map.containsKey(unified)) {
+ throw new IllegalArgumentException(String.format("Duplicate pattern [%s]", unified));
+ }
+ map.put(unified, mappingContext);
+ }
+
+ @Override
+ public MappingContext<V> match(final String path) {
+ List<MappingContext<V>> hits = new ArrayList<>();
+ for (Map.Entry<String, MappingContext<V>> entry : map.entrySet()) {
+ final String pattern = entry.getKey();
+ if (pattern.equals(path)) {
+ return entry.getValue();
+ }
+ if (pathMatcher.matches(pattern, path)) {
+ hits.add(entry.getValue());
+ }
+ }
+ if (hits.isEmpty()) {
+ return null;
+ }
+ if (1 < hits.size()) {
+ hits.sort(new MappingComparator().reversed());
+ }
+ return hits.get(0);
+ }
+
+ private String unifyPattern(final String pattern) {
+ return TEMPLATE_PATTERN.matcher(pattern).replaceAll("[^/]+");
+ }
+
+ static class MappingComparator implements Comparator<MappingContext<?>> {
+
+ @Override
+ public int compare(final MappingContext<?> o1, final MappingContext<?> o2) {
+ String[] s1 = o1.pattern().split(PATH_SEPARATOR);
+ String[] s2 = o2.pattern().split(PATH_SEPARATOR);
+ int len = Math.min(s1.length, s2.length);
+ for (int i = 0; i < len; i++) {
+ if (isTemplate(s1[i]) && !isTemplate(s2[i])) {
+ return -1;
+ }
+ if (!isTemplate(s1[i]) && isTemplate(s2[i])) {
+ return 1;
+ }
+ }
+ throw new AmbiguousPathPatternException(MessageFormat.format("Ambiguous path pattern: [{0}], [{1}].", o1.pattern(), o2.pattern()));
+ }
+
+ private static boolean isTemplate(final String fragment) {
+ return fragment.startsWith("{") && fragment.endsWith("}");
+ }
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/UrlPatternMap.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/UrlPatternMap.java
new file mode 100644
index 0000000..cd96c46
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/mapping/UrlPatternMap.java
@@ -0,0 +1,43 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.mapping;
+
+/**
+ * UrlPatternMap is used for path pattern storage and path matching.
+ * {@link MappingContext} is an object holding path pattern and payload.
+ *
+ * @param <V> Type of payload
+ */
+public interface UrlPatternMap<V> {
+
+ /**
+ * Add a path pattern and value to UrlPatternMap.
+ *
+ * @param pathPattern Path pattern
+ * @param value Payload of the path pattern
+ */
+ void put(String pathPattern, V value);
+
+ /**
+ * Find a proper MappingContext for current path.
+ *
+ * @param path A path to match.
+ * @return A MappingContext if the path matched a pattern. Return null if mismatched.
+ */
+ MappingContext<V> match(String path);
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/ExceptionHandling.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/ExceptionHandling.java
new file mode 100644
index 0000000..1603b81
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/ExceptionHandling.java
@@ -0,0 +1,102 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pipeline;
+
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.HttpVersion;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.shardingsphere.elasticjob.restful.handler.ExceptionHandleResult;
+import org.apache.shardingsphere.elasticjob.restful.handler.ExceptionHandler;
+import org.apache.shardingsphere.elasticjob.restful.Http;
+import org.apache.shardingsphere.elasticjob.restful.serializer.ResponseBodySerializer;
+import org.apache.shardingsphere.elasticjob.restful.serializer.ResponseBodySerializerFactory;
+
+import java.util.Map;
+
+/**
+ * Catch exceptions and look for a ExceptionHandler.
+ */
+@Sharable
+public final class ExceptionHandling extends ChannelInboundHandlerAdapter {
+
+ private static final DefaultExceptionHandler DEFAULT_EXCEPTION_HANDLER = new DefaultExceptionHandler();
+
+ private final Map<Class<? extends Throwable>, ExceptionHandler<? extends Throwable>> exceptionHandlers;
+
+ public ExceptionHandling(final Map<Class<? extends Throwable>, ExceptionHandler<? extends Throwable>> exceptionHandlers) {
+ this.exceptionHandlers = exceptionHandlers;
+ }
+
+ @Override
+ public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception {
+ ExceptionHandler<Throwable> exceptionHandler = searchExceptionHandler(cause);
+ ExceptionHandleResult handleResult = exceptionHandler.handleException(cause);
+ String mimeType = HttpUtil.getMimeType(handleResult.getContentType()).toString();
+ ResponseBodySerializer serializer = ResponseBodySerializerFactory.getResponseBodySerializer(mimeType);
+ byte[] body = serializer.serialize(handleResult.getResult());
+ FullHttpResponse response = createHttpResponse(handleResult.getStatusCode(), handleResult.getContentType(), body);
+ ctx.writeAndFlush(response);
+ }
+
+ private FullHttpResponse createHttpResponse(final int statusCode, final String contentType, final byte[] body) {
+ FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(statusCode), Unpooled.copiedBuffer(body));
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType);
+ HttpUtil.setContentLength(response, body.length);
+ return response;
+ }
+
+ private <T extends Throwable> ExceptionHandler<T> searchExceptionHandler(final Throwable cause) {
+ Class<? extends Throwable> exceptionType = cause.getClass();
+ ExceptionHandler<? extends Throwable> exceptionHandler = exceptionHandlers.get(exceptionType);
+ if (null == exceptionHandler) {
+ for (Map.Entry<Class<? extends Throwable>, ExceptionHandler<? extends Throwable>> entry : exceptionHandlers.entrySet()) {
+ Class<? extends Throwable> clazz = entry.getKey();
+ ExceptionHandler<? extends Throwable> handler = entry.getValue();
+ if (clazz.isAssignableFrom(exceptionType)) {
+ exceptionHandler = handler;
+ break;
+ }
+ }
+ }
+ if (null == exceptionHandler) {
+ exceptionHandler = DEFAULT_EXCEPTION_HANDLER;
+ }
+ return (ExceptionHandler<T>) exceptionHandler;
+ }
+
+ @Slf4j
+ private static class DefaultExceptionHandler implements ExceptionHandler<Throwable> {
+
+ @Override
+ public ExceptionHandleResult handleException(final Throwable ex) {
+ return ExceptionHandleResult.builder()
+ .statusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code())
+ .contentType(Http.DEFAULT_CONTENT_TYPE)
+ .result(ex)
+ .build();
+ }
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HandleMethodExecutor.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HandleMethodExecutor.java
new file mode 100644
index 0000000..2dcfa92
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HandleMethodExecutor.java
@@ -0,0 +1,76 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pipeline;
+
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.HttpVersion;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandleContext;
+import org.apache.shardingsphere.elasticjob.restful.handler.Handler;
+import org.apache.shardingsphere.elasticjob.restful.serializer.ResponseBodySerializer;
+import org.apache.shardingsphere.elasticjob.restful.serializer.ResponseBodySerializerFactory;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * The handler which actually executes handle method and creates HTTP response for responding.
+ * If an exception occurred when executing handle method, this handler would pass it to Handler named {@link ExceptionHandling}.
+ */
+@Sharable
+public final class HandleMethodExecutor extends ChannelInboundHandlerAdapter {
+
+ @Override
+ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
+ HandleContext<Handler> handleContext = (HandleContext<Handler>) msg;
+ Handler handler = handleContext.getMappingContext().payload();
+ Object[] args = handleContext.getArgs();
+
+ Object handleResult = handler.execute(args);
+
+ String mimeType = HttpUtil.getMimeType(handler.getProducing()).toString();
+ ResponseBodySerializer serializer = ResponseBodySerializerFactory.getResponseBodySerializer(mimeType);
+ byte[] bodyBytes = serializer.serialize(handleResult);
+ FullHttpResponse response = createHttpResponse(handler.getProducing(), bodyBytes, handler.getHttpStatusCode());
+ ctx.writeAndFlush(response);
+ }
+
+ private FullHttpResponse createHttpResponse(final String producingContentType, final byte[] bodyBytes, final int statusCode) {
+ HttpResponseStatus httpResponseStatus = HttpResponseStatus.valueOf(statusCode);
+ FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, httpResponseStatus, Unpooled.wrappedBuffer(bodyBytes));
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, producingContentType);
+ HttpUtil.setContentLength(response, bodyBytes.length);
+ HttpUtil.setKeepAlive(response, true);
+ return response;
+ }
+
+ @Override
+ public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception {
+ if (cause instanceof InvocationTargetException) {
+ ctx.fireExceptionCaught(cause.getCause());
+ } else {
+ ctx.fireExceptionCaught(cause);
+ }
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HandlerParameterDecoder.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HandlerParameterDecoder.java
new file mode 100644
index 0000000..736f198
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HandlerParameterDecoder.java
@@ -0,0 +1,157 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pipeline;
+
+import com.google.common.base.Preconditions;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.UnsupportedMessageTypeException;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.QueryStringDecoder;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandleContext;
+import org.apache.shardingsphere.elasticjob.restful.handler.Handler;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandlerParameter;
+import org.apache.shardingsphere.elasticjob.restful.Http;
+import org.apache.shardingsphere.elasticjob.restful.mapping.MappingContext;
+import org.apache.shardingsphere.elasticjob.restful.mapping.PathMatcher;
+import org.apache.shardingsphere.elasticjob.restful.mapping.RegexPathMatcher;
+import org.apache.shardingsphere.elasticjob.restful.deserializer.RequestBodyDeserializer;
+import org.apache.shardingsphere.elasticjob.restful.deserializer.RequestBodyDeserializerFactory;
+
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * This handler is used for preparing parameters before executing handle method.
+ * It prepares arguments declared by {@link org.apache.shardingsphere.elasticjob.restful.annotation.Param}
+ * and {@link org.apache.shardingsphere.elasticjob.restful.annotation.RequestBody}, and deserializes arguments to declared type.
+ */
+@Slf4j
+@Sharable
+public final class HandlerParameterDecoder extends ChannelInboundHandlerAdapter {
+
+ private final PathMatcher pathMatcher = new RegexPathMatcher();
+
+ @Override
+ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
+ HandleContext<Handler> handleContext = (HandleContext<Handler>) msg;
+ FullHttpRequest httpRequest = handleContext.getHttpRequest();
+ MappingContext<Handler> mappingContext = handleContext.getMappingContext();
+ Object[] arguments = prepareArguments(httpRequest, mappingContext);
+ handleContext.setArgs(arguments);
+ ctx.fireChannelRead(handleContext);
+ }
+
+ private Object[] prepareArguments(final FullHttpRequest httpRequest, final MappingContext<Handler> mappingContext) {
+ Handler handler = mappingContext.payload();
+ List<HandlerParameter> handlerParameters = handler.getHandlerParameters();
+ Map<String, List<String>> queryParameters = parseQuery(httpRequest.uri());
+ Map<String, String> templateVariables = pathMatcher.captureVariables(mappingContext.pattern(), httpRequest.uri());
+ Object[] args = new Object[handlerParameters.size()];
+ boolean requestBodyAlreadyParsed = false;
+ for (int i = 0; i < handlerParameters.size(); i++) {
+ HandlerParameter handlerParameter = handlerParameters.get(i);
+ Object parsedValue = null;
+ switch (handlerParameter.getParamSource()) {
+ case PATH:
+ parsedValue = deserializeBuiltInType(handlerParameter.getType(), templateVariables.get(handlerParameter.getName()));
+ break;
+ case QUERY:
+ List<String> queryValues = queryParameters.get(handlerParameter.getName());
+ parsedValue = deserializeQueryParameter(handlerParameter.getType(), queryValues);
+ break;
+ case HEADER:
+ String headerValue = httpRequest.headers().get(handlerParameter.getName());
+ parsedValue = deserializeBuiltInType(handlerParameter.getType(), headerValue);
+ break;
+ case BODY:
+ Preconditions.checkState(!requestBodyAlreadyParsed, "@RequestBody duplicated on handle method.");
+ byte[] bytes = ByteBufUtil.getBytes(httpRequest.content());
+ String mimeType = Optional.ofNullable(HttpUtil.getMimeType(httpRequest))
+ .orElseGet(() -> HttpUtil.getMimeType(Http.DEFAULT_CONTENT_TYPE)).toString();
+ RequestBodyDeserializer deserializer = RequestBodyDeserializerFactory.getRequestBodyDeserializer(mimeType);
+ if (null == deserializer) {
+ throw new UnsupportedMessageTypeException(MessageFormat.format("Unsupported MIME type [{0}]", mimeType));
+ }
+ parsedValue = deserializer.deserialize(handlerParameter.getType(), bytes);
+ requestBodyAlreadyParsed = true;
+ break;
+ case UNKNOWN:
+ log.warn("Unknown source argument [{}] on index [{}].", handlerParameter.getName(), handlerParameter.getIndex());
+ break;
+ default:
+ }
+ args[i] = parsedValue;
+ }
+ return args;
+ }
+
+ private Map<String, List<String>> parseQuery(final String uri) {
+ QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri);
+ return queryStringDecoder.parameters();
+ }
+
+ private Object deserializeQueryParameter(final Class<?> targetType, final List<String> queryValues) {
+ if (null == queryValues || queryValues.isEmpty()) {
+ return null;
+ }
+ if (1 == queryValues.size()) {
+ return deserializeBuiltInType(targetType, queryValues.get(0));
+ }
+ throw new UnsupportedOperationException("Multi value query doesn't support yet.");
+ }
+
+ private Object deserializeBuiltInType(final Class<?> targetType, final String value) {
+ Preconditions.checkArgument(!value.isEmpty(), "Cannot deserialize empty value.");
+ if (String.class.equals(targetType)) {
+ return value;
+ }
+ if (Boolean.class.equals(targetType) || boolean.class.equals(targetType)) {
+ return Boolean.parseBoolean(value);
+ }
+ if (Character.class.equals(targetType) || char.class.equals(targetType)) {
+ Preconditions.checkArgument(1 >= value.length(), MessageFormat.format("Cannot set value [{0}] into a char.", value));
+ return value.charAt(0);
+ }
+ if (Byte.class.equals(targetType) || byte.class.equals(targetType)) {
+ return Byte.parseByte(value);
+ }
+ if (Short.class.equals(targetType) || short.class.equals(targetType)) {
+ return Short.parseShort(value);
+ }
+ if (Integer.class.equals(targetType) || int.class.equals(targetType)) {
+ return Integer.parseInt(value);
+ }
+ if (Long.class.equals(targetType) || long.class.equals(targetType)) {
+ return Long.parseLong(value);
+ }
+ if (Float.class.equals(targetType) || float.class.equals(targetType)) {
+ return Float.parseFloat(value);
+ }
+ if (Double.class.equals(targetType) || double.class.equals(targetType)) {
+ return Double.parseDouble(value);
+ }
+ throw new IllegalArgumentException(MessageFormat.format("Cannot deserialize path variable [{0}] into [{1}]", value, targetType.getName()));
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HttpRequestDispatcher.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HttpRequestDispatcher.java
new file mode 100644
index 0000000..0fa26f1
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HttpRequestDispatcher.java
@@ -0,0 +1,81 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pipeline;
+
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpMethod;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandleContext;
+import org.apache.shardingsphere.elasticjob.restful.handler.Handler;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandlerMappingRegistry;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandlerNotFoundException;
+import org.apache.shardingsphere.elasticjob.restful.mapping.MappingContext;
+import org.apache.shardingsphere.elasticjob.restful.RestfulController;
+import org.apache.shardingsphere.elasticjob.restful.annotation.ContextPath;
+import org.apache.shardingsphere.elasticjob.restful.annotation.Mapping;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * If a HTTP request reached, HttpRequestDispatcher would lookup a proper Handler for the request.
+ * Assemble a {@link HandleContext} with HTTP request and {@link MappingContext}, then pass it to the next in-bound handler.
+ */
+@Sharable
+@Slf4j
+public final class HttpRequestDispatcher extends ChannelInboundHandlerAdapter {
+
+ private final HandlerMappingRegistry mappingRegistry = new HandlerMappingRegistry();
+
+ public HttpRequestDispatcher(final List<RestfulController> restfulControllers) {
+ initMappingRegistry(restfulControllers);
+ }
+
+ @Override
+ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
+ log.debug("{}", msg);
+ FullHttpRequest request = (FullHttpRequest) msg;
+ MappingContext<Handler> mappingContext = mappingRegistry.getMappingContext(request);
+ if (null == mappingContext) {
+ throw new HandlerNotFoundException(request.uri());
+ }
+ HandleContext<Handler> handleContext = new HandleContext<>(request, mappingContext);
+ ctx.fireChannelRead(handleContext);
+ }
+
+ private void initMappingRegistry(final List<RestfulController> restfulControllers) {
+ for (RestfulController restfulController : restfulControllers) {
+ Class<? extends RestfulController> controllerClass = restfulController.getClass();
+ String contextPath = Optional.ofNullable(controllerClass.getAnnotation(ContextPath.class)).map(ContextPath::value).orElse("");
+ for (Method method : controllerClass.getMethods()) {
+ Mapping mapping = method.getAnnotation(Mapping.class);
+ if (null == mapping) {
+ continue;
+ }
+ HttpMethod httpMethod = HttpMethod.valueOf(mapping.method());
+ String pattern = mapping.path();
+ String fullPathPattern = contextPath + pattern;
+ mappingRegistry.addMapping(httpMethod, fullPathPattern, new Handler(restfulController, method));
+ }
+ }
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/RestfulServiceChannelInitializer.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/RestfulServiceChannelInitializer.java
new file mode 100644
index 0000000..bd261a9
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/RestfulServiceChannelInitializer.java
@@ -0,0 +1,57 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pipeline;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import org.apache.shardingsphere.elasticjob.restful.NettyRestfulServiceConfiguration;
+
+/**
+ * Initialize channel pipeline.
+ */
+public final class RestfulServiceChannelInitializer extends ChannelInitializer<Channel> {
+
+ private final HttpRequestDispatcher httpRequestDispatcher;
+
+ private final HandlerParameterDecoder handlerParameterDecoder;
+
+ private final HandleMethodExecutor handleMethodExecutor;
+
+ private final ExceptionHandling exceptionHandling;
+
+ public RestfulServiceChannelInitializer(final NettyRestfulServiceConfiguration configuration) {
+ httpRequestDispatcher = new HttpRequestDispatcher(configuration.getControllerInstances());
+ handlerParameterDecoder = new HandlerParameterDecoder();
+ handleMethodExecutor = new HandleMethodExecutor();
+ exceptionHandling = new ExceptionHandling(configuration.getExceptionHandlers());
+ }
+
+ @Override
+ protected void initChannel(final Channel channel) throws Exception {
+ ChannelPipeline pipeline = channel.pipeline();
+ pipeline.addLast("codec", new HttpServerCodec());
+ pipeline.addLast("aggregator", new HttpObjectAggregator(1024 * 1024));
+ pipeline.addLast("dispatcher", httpRequestDispatcher);
+ pipeline.addLast("handlerParameterDecoder", handlerParameterDecoder);
+ pipeline.addLast("handleMethodExecutor", handleMethodExecutor);
+ pipeline.addLast("exceptionHandling", exceptionHandling);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/serializer/ResponseBodySerializer.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/serializer/ResponseBodySerializer.java
new file mode 100644
index 0000000..6c5c22d
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/serializer/ResponseBodySerializer.java
@@ -0,0 +1,39 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.serializer;
+
+/**
+ * Serializer for serializing response body with specific MIME type.
+ */
+public interface ResponseBodySerializer {
+
+ /**
+ * Specify which type would be serialized by this serializer.
+ *
+ * @return MIME type
+ */
+ String mimeType();
+
+ /**
+ * Serialize object to bytes.
+ *
+ * @param responseBody Object to be serialized
+ * @return bytes
+ */
+ byte[] serialize(Object responseBody);
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/serializer/ResponseBodySerializerFactory.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/serializer/ResponseBodySerializerFactory.java
new file mode 100644
index 0000000..118dfb3
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/serializer/ResponseBodySerializerFactory.java
@@ -0,0 +1,50 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.serializer;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Response body serializer factory.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class ResponseBodySerializerFactory {
+
+ private static final Map<String, ResponseBodySerializer> RESPONSE_BODY_SERIALIZERS = new ConcurrentHashMap<>();
+
+ static {
+ for (ResponseBodySerializer serializer : ServiceLoader.load(ResponseBodySerializer.class)) {
+ RESPONSE_BODY_SERIALIZERS.put(serializer.mimeType(), serializer);
+ }
+ }
+
+ /**
+ * Get serializer for specific HTTP content type.
+ *
+ * @param contentType HTTP content type
+ * @return Serializer
+ */
+ public static ResponseBodySerializer getResponseBodySerializer(final String contentType) {
+ return RESPONSE_BODY_SERIALIZERS.get(contentType);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/serializer/impl/JsonResponseBodySerializer.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/serializer/impl/JsonResponseBodySerializer.java
new file mode 100644
index 0000000..5552fcb
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/serializer/impl/JsonResponseBodySerializer.java
@@ -0,0 +1,42 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.serializer.impl;
+
+import com.google.gson.Gson;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import org.apache.shardingsphere.elasticjob.restful.serializer.ResponseBodySerializer;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Serializer for <code>application/json</code>.
+ */
+public final class JsonResponseBodySerializer implements ResponseBodySerializer {
+
+ private final Gson gson = new Gson();
+
+ @Override
+ public String mimeType() {
+ return HttpHeaderValues.APPLICATION_JSON.toString();
+ }
+
+ @Override
+ public byte[] serialize(final Object responseBody) {
+ return gson.toJson(responseBody).getBytes(StandardCharsets.UTF_8);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/resources/META-INF/services/org.apache.shardingsphere.elasticjob.restful.deserializer.RequestBodyDeserializer b/elasticjob-infra/elasticjob-restful/src/main/resources/META-INF/services/org.apache.shardingsphere.elasticjob.restful.deserializer.RequestBodyDeserializer
new file mode 100644
index 0000000..b0a4b73
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/resources/META-INF/services/org.apache.shardingsphere.elasticjob.restful.deserializer.RequestBodyDeserializer
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+
+org.apache.shardingsphere.elasticjob.restful.deserializer.impl.JsonRequestBodyDeserializer
+org.apache.shardingsphere.elasticjob.restful.deserializer.impl.TextPlainRequestBodyDeserializer
diff --git a/elasticjob-infra/elasticjob-restful/src/main/resources/META-INF/services/org.apache.shardingsphere.elasticjob.restful.serializer.ResponseBodySerializer b/elasticjob-infra/elasticjob-restful/src/main/resources/META-INF/services/org.apache.shardingsphere.elasticjob.restful.serializer.ResponseBodySerializer
new file mode 100644
index 0000000..5b02313
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/resources/META-INF/services/org.apache.shardingsphere.elasticjob.restful.serializer.ResponseBodySerializer
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.apache.shardingsphere.elasticjob.restful.serializer.impl.JsonResponseBodySerializer
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/RegexPathMatcherTest.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/RegexPathMatcherTest.java
new file mode 100644
index 0000000..68b0777
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/RegexPathMatcherTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.shardingsphere.elasticjob.restful;
+
+import org.apache.shardingsphere.elasticjob.restful.mapping.PathMatcher;
+import org.apache.shardingsphere.elasticjob.restful.mapping.RegexPathMatcher;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class RegexPathMatcherTest {
+
+ @Test
+ public void assertCaptureTemplate() {
+ PathMatcher pathMatcher = new RegexPathMatcher();
+ Map<String, String> variables = pathMatcher.captureVariables("/app/{jobName}/disable/{until}/done", "/app/myJob/disable/20201231/done?name=some_name&value=some_value");
+ assertFalse(variables.isEmpty());
+ assertThat(variables.size(), is(2));
+ assertThat(variables.get("jobName"), is("myJob"));
+ assertThat(variables.get("until"), is("20201231"));
+ assertNull(variables.get("app"));
+ }
+
+ @Test
+ public void assertCapturePatternWithoutTemplate() {
+ PathMatcher pathMatcher = new RegexPathMatcher();
+ Map<String, String> variables = pathMatcher.captureVariables("/app", "/app");
+ assertTrue(variables.isEmpty());
+ }
+
+ @Test
+ public void assertPathMatch() {
+ PathMatcher pathMatcher = new RegexPathMatcher();
+ assertTrue(pathMatcher.matches("/app/{jobName}", "/app/myJob"));
+ }
+
+ @Test
+ public void assertValidatePathPattern() {
+ PathMatcher pathMatcher = new RegexPathMatcher();
+ assertTrue(pathMatcher.isValidPathPattern("/"));
+ assertTrue(pathMatcher.isValidPathPattern("/app/job"));
+ assertTrue(pathMatcher.isValidPathPattern("/app/{jobName}"));
+ assertTrue(pathMatcher.isValidPathPattern("/{appName}/{jobName}/status"));
+ assertFalse(pathMatcher.isValidPathPattern("/app/jobName}"));
+ assertFalse(pathMatcher.isValidPathPattern("/app/{jobName"));
+ assertFalse(pathMatcher.isValidPathPattern("/app/{job}Name"));
+ assertFalse(pathMatcher.isValidPathPattern(""));
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/RegexUrlPatternMapTest.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/RegexUrlPatternMapTest.java
new file mode 100644
index 0000000..c1fb645
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/RegexUrlPatternMapTest.java
@@ -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.shardingsphere.elasticjob.restful;
+
+import org.apache.shardingsphere.elasticjob.restful.mapping.MappingContext;
+import org.apache.shardingsphere.elasticjob.restful.mapping.RegexUrlPatternMap;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+public class RegexUrlPatternMapTest {
+
+ @Test
+ public void assertRegexUrlPatternMap() {
+ RegexUrlPatternMap<Integer> urlPatternMap = new RegexUrlPatternMap<>();
+ urlPatternMap.put("/app/{jobName}", 1);
+ urlPatternMap.put("/app/list", 2);
+ urlPatternMap.put("/app/{jobName}/disable", 3);
+ urlPatternMap.put("/app/{jobName}/enable", 4);
+ MappingContext<Integer> mappingContext = urlPatternMap.match("/app/myJob");
+ assertNotNull(mappingContext);
+ assertThat(mappingContext.pattern(), is("/app/{jobName}"));
+ assertThat(mappingContext.payload(), is(1));
+ mappingContext = urlPatternMap.match("/app/list");
+ assertNotNull(mappingContext);
+ assertThat(mappingContext.pattern(), is("/app/list"));
+ assertThat(mappingContext.payload(), is(2));
+ mappingContext = urlPatternMap.match("/job/list");
+ assertNull(mappingContext);
+ }
+
+ @Test
+ public void assertAmbiguous() {
+ RegexUrlPatternMap<Integer> urlPatternMap = new RegexUrlPatternMap<>();
+ urlPatternMap.put("/foo/{bar}/{fooName}/status", 10);
+ urlPatternMap.put("/foo/{bar}/operate/{metrics}", 11);
+ MappingContext<Integer> mappingContext = urlPatternMap.match("/foo/barValue/operate/status");
+ assertNotNull(mappingContext);
+ assertThat(mappingContext.pattern(), is("/foo/{bar}/operate/{metrics}"));
+ assertThat(mappingContext.payload(), is(11));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void assertDuplicate() {
+ RegexUrlPatternMap<Integer> urlPatternMap = new RegexUrlPatternMap<>();
+ urlPatternMap.put("/app/{jobName}/enable", 0);
+ urlPatternMap.put("/app/{jobName}", 1);
+ urlPatternMap.put("/app/{appName}", 2);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/controller/JobController.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/controller/JobController.java
new file mode 100644
index 0000000..aef76ec
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/controller/JobController.java
@@ -0,0 +1,90 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.controller;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.shardingsphere.elasticjob.restful.Http;
+import org.apache.shardingsphere.elasticjob.restful.annotation.ParamSource;
+import org.apache.shardingsphere.elasticjob.restful.RestfulController;
+import org.apache.shardingsphere.elasticjob.restful.annotation.ContextPath;
+import org.apache.shardingsphere.elasticjob.restful.annotation.Mapping;
+import org.apache.shardingsphere.elasticjob.restful.annotation.Param;
+import org.apache.shardingsphere.elasticjob.restful.annotation.RequestBody;
+import org.apache.shardingsphere.elasticjob.restful.annotation.Returning;
+import org.apache.shardingsphere.elasticjob.restful.pojo.JobPojo;
+
+@Slf4j
+@ContextPath("/job")
+public class JobController implements RestfulController {
+
+ /**
+ * Pretend to create a job.
+ *
+ * @param group Group
+ * @param jobName Job name
+ * @param cron Job cron
+ * @param description Job description
+ * @return Result
+ */
+ @Mapping(method = Http.POST, path = "/{group}/{jobName}")
+ public JobPojo createJob(@Param(name = "group", source = ParamSource.PATH) final String group,
+ @Param(name = "jobName", source = ParamSource.PATH) final String jobName,
+ @Param(name = "cron", source = ParamSource.QUERY) final String cron,
+ @RequestBody final String description) {
+ JobPojo jobPojo = new JobPojo();
+ jobPojo.setName(jobName);
+ jobPojo.setCron(cron);
+ jobPojo.setGroup(group);
+ jobPojo.setDescription(description);
+ return jobPojo;
+ }
+
+ /**
+ * Throw an IllegalStateException.
+ *
+ * @param message Exception message
+ * @return None
+ */
+ @Mapping(method = Http.GET, path = "/throw/IllegalState")
+ public Object throwIllegalStateException(@Param(name = "Exception-Message", source = ParamSource.HEADER) final String message) {
+ throw new IllegalStateException(message);
+ }
+
+ /**
+ * Throw an IllegalArgumentException.
+ *
+ * @param message Exception message
+ * @return None
+ */
+ @Mapping(method = Http.GET, path = "/throw/IllegalArgument")
+ public Object throwIllegalArgumentException(@Param(name = "Exception-Message", source = ParamSource.HEADER) final String message) {
+ throw new IllegalArgumentException(message);
+ }
+
+ /**
+ * Return 204.
+ *
+ * @param noop Useless
+ * @return None
+ */
+ @Mapping(method = Http.GET, path = "/code/204")
+ @Returning(code = 204)
+ public Object return204(final String noop) {
+ return null;
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/handler/CustomIllegalStateExceptionHandler.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/handler/CustomIllegalStateExceptionHandler.java
new file mode 100644
index 0000000..95c5d28
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/handler/CustomIllegalStateExceptionHandler.java
@@ -0,0 +1,33 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.handler;
+
+import org.apache.shardingsphere.elasticjob.restful.Http;
+import org.apache.shardingsphere.elasticjob.restful.pojo.ResultDto;
+
+public class CustomIllegalStateExceptionHandler implements ExceptionHandler<IllegalStateException> {
+
+ @Override
+ public ExceptionHandleResult handleException(final IllegalStateException ex) {
+ return ExceptionHandleResult.builder()
+ .statusCode(403)
+ .contentType(Http.DEFAULT_CONTENT_TYPE)
+ .result(ResultDto.builder().code(1).data(ex.getLocalizedMessage()).build())
+ .build();
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HttpClient.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HttpClient.java
new file mode 100644
index 0000000..410c1b3
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HttpClient.java
@@ -0,0 +1,80 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pipeline;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpClientCodec;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.SneakyThrows;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class HttpClient {
+
+ /**
+ * Send a HTTP request and invoke consumer when server response.
+ *
+ * @param host Server host
+ * @param port Server port
+ * @param request HTTP request
+ * @param consumer HTTP response consumer
+ * @param timeoutSeconds Wait for consume
+ */
+ @SneakyThrows
+ public static void request(final String host, final int port, final FullHttpRequest request, final Consumer<FullHttpResponse> consumer, final Long timeoutSeconds) {
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+ EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
+ Channel channel = new Bootstrap()
+ .group(eventLoopGroup)
+ .channel(NioSocketChannel.class)
+ .remoteAddress(host, port)
+ .handler(new ChannelInitializer<Channel>() {
+ @Override
+ protected void initChannel(final Channel ch) throws Exception {
+ ch.pipeline()
+ .addLast(new HttpClientCodec())
+ .addLast(new HttpObjectAggregator(1024 * 1024))
+ .addLast(new SimpleChannelInboundHandler<FullHttpResponse>() {
+ @Override
+ protected void channelRead0(final ChannelHandlerContext ctx, final FullHttpResponse httpResponse) throws Exception {
+ consumer.accept(httpResponse);
+ countDownLatch.countDown();
+ }
+ });
+ }
+ }).connect()
+ .sync().channel();
+ channel.writeAndFlush(request);
+ countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
+ channel.close().sync();
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HttpRequestDispatcherTest.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HttpRequestDispatcherTest.java
new file mode 100644
index 0000000..e55fcf9
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/HttpRequestDispatcherTest.java
@@ -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.shardingsphere.elasticjob.restful.pipeline;
+
+import com.google.common.collect.Lists;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.http.DefaultFullHttpRequest;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpVersion;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandlerNotFoundException;
+import org.apache.shardingsphere.elasticjob.restful.controller.JobController;
+import org.junit.Test;
+
+public class HttpRequestDispatcherTest {
+
+ @Test(expected = HandlerNotFoundException.class)
+ public void assertDispatcherHandlerNotFound() {
+ EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDispatcher(Lists.newArrayList(new JobController())));
+ FullHttpRequest fullHttpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/hello/myJob/myCron");
+ channel.writeInbound(fullHttpRequest);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/NettyRestfulServiceTest.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/NettyRestfulServiceTest.java
new file mode 100644
index 0000000..87ca121
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/NettyRestfulServiceTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pipeline;
+
+import com.google.gson.Gson;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.http.DefaultFullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.HttpVersion;
+import lombok.SneakyThrows;
+import org.apache.shardingsphere.elasticjob.restful.NettyRestfulService;
+import org.apache.shardingsphere.elasticjob.restful.NettyRestfulServiceConfiguration;
+import org.apache.shardingsphere.elasticjob.restful.RestfulService;
+import org.apache.shardingsphere.elasticjob.restful.controller.JobController;
+import org.apache.shardingsphere.elasticjob.restful.handler.CustomIllegalStateExceptionHandler;
+import org.apache.shardingsphere.elasticjob.restful.pojo.JobPojo;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+public class NettyRestfulServiceTest {
+
+ private static final String HOST = "localhost";
+
+ private static final int PORT = 18080;
+
+ private static RestfulService restfulService;
+
+ @BeforeClass
+ public static void init() {
+ NettyRestfulServiceConfiguration configuration = new NettyRestfulServiceConfiguration(PORT);
+ configuration.setHost(HOST);
+ configuration.addControllerInstance(new JobController());
+ configuration.addExceptionHandler(IllegalStateException.class, new CustomIllegalStateExceptionHandler());
+ restfulService = new NettyRestfulService(configuration);
+ restfulService.startup();
+ }
+
+ @SneakyThrows
+ @Test(timeout = 10000L)
+ public void assertRequestWithParameters() {
+ String cron = "0 * * * * ?";
+ String uri = String.format("/job/myGroup/myJob?cron=%s", URLEncoder.encode(cron, "UTF-8"));
+ String description = "Descriptions about this job.";
+ byte[] body = description.getBytes(StandardCharsets.UTF_8);
+ DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uri, Unpooled.wrappedBuffer(body));
+ request.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
+ HttpUtil.setContentLength(request, body.length);
+ HttpClient.request(HOST, PORT, request, httpResponse -> {
+ assertEquals(200, httpResponse.status().code());
+ byte[] bytes = ByteBufUtil.getBytes(httpResponse.content());
+ String json = new String(bytes, StandardCharsets.UTF_8);
+ Gson gson = new Gson();
+ JobPojo jobPojo = gson.fromJson(json, JobPojo.class);
+ assertThat(jobPojo.getCron(), is(cron));
+ assertThat(jobPojo.getGroup(), is("myGroup"));
+ assertThat(jobPojo.getName(), is("myJob"));
+ assertThat(jobPojo.getDescription(), is(description));
+ }, 10000L);
+ }
+
+ @Test(timeout = 10000L)
+ public void assertCustomExceptionHandler() {
+ DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/job/throw/IllegalState");
+ request.headers().set("Exception-Message", "An illegal state exception message.");
+ HttpClient.request(HOST, PORT, request, httpResponse -> {
+ // Handle by CustomExceptionHandler
+ assertThat(httpResponse.status().code(), is(403));
+ }, 10000L);
+ }
+
+ @Test(timeout = 10000L)
+ public void assertUsingDefaultExceptionHandler() {
+ DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/job/throw/IllegalArgument");
+ request.headers().set("Exception-Message", "An illegal argument exception message.");
+ HttpClient.request(HOST, PORT, request, httpResponse -> {
+ // Handle by DefaultExceptionHandler
+ assertThat(httpResponse.status().code(), is(500));
+ }, 10000L);
+ }
+
+ @Test(timeout = 10000L)
+ public void assertReturnStatusCode() {
+ DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/job/code/204");
+ HttpClient.request(HOST, PORT, request, httpResponse -> {
+ assertThat(httpResponse.status().code(), is(204));
+ }, 10000L);
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ if (null != restfulService) {
+ restfulService.shutdown();
+ }
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pojo/JobPojo.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pojo/JobPojo.java
new file mode 100644
index 0000000..bef033f
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pojo/JobPojo.java
@@ -0,0 +1,37 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pojo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * A simple POJO for test.
+ */
+@Getter
+@Setter
+public class JobPojo {
+
+ private String group;
+
+ private String name;
+
+ private String cron;
+
+ private String description;
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pojo/ResultDto.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pojo/ResultDto.java
new file mode 100644
index 0000000..6f870c9
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pojo/ResultDto.java
@@ -0,0 +1,30 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pojo;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@Getter
+public class ResultDto<T> {
+
+ private final int code;
+
+ private final T data;
+}
diff --git a/elasticjob-infra/pom.xml b/elasticjob-infra/pom.xml
index a9eb5ca..587b797 100644
--- a/elasticjob-infra/pom.xml
+++ b/elasticjob-infra/pom.xml
@@ -31,5 +31,6 @@
<module>elasticjob-infra-common</module>
<module>elasticjob-registry-center</module>
<module>elasticjob-tracing</module>
+ <module>elasticjob-restful</module>
</modules>
</project>