Как реализовать многопоточность в Jetty
Реализую сервис, которому в GET-Запросе передаются некоторые теги, а сервис в свою очередь обращается на StackOverFlow API, ищет указанные теги и в JSON формате возвращает нам количество существующих по данному тегу вопросов и кол-во вопросов на которые дали ответ. Выглядит это примерно следующим образом : 
Однако существует некоторая проблема, при указывании пользователем нескольких тэгов сервис должен обрабатывать их параллельно и независимо друг от друга, а сейчас код реализован иначе. Я уже гуглил и пытался реализовывать через футуры и пока загадочные для меня CompletableFuture, однако сервис вообще перестает работать в этом случае.
Main класс:
public class Main {
public static void main(String[] args) {
ServletContextHandler context = new ServletContextHandler(1);
context.addServlet(new ServletHolder(new AsyncServer()), "/*");
Server server = new Server(8080);
server.setHandler(context);
try {
server.start();
server.join();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Два сервлета: Первый вызывает второй. Первый:
public class AsyncServer extends HttpServlet {
Gson gson = new Gson();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
final AsyncContext ctxt = req.startAsync();
String[] tags = req.getParameterValues("tag");
for (String tag: tags) {
ctxt.start(new Runnable() {
@Override
public void run() {
try {
StackOverFlowServlet servlet = new StackOverFlowServlet();
Root root = getStackOverFlow(tag);
servlet.setParameter(tag, gson, root);
servlet.doGet(req, resp);
ctxt.complete();
} catch (ServletException | IOException e) {
e.printStackTrace();
}
}
}
);
}
}
public Root getStackOverFlow(String tag) throws IOException {
URL url = new URL("https://api.stackexchange.com/2.2/search?pagesize=100&order=desc&sort=creation&tagged=" + tag + "&site=stackoverflow");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
GZIPInputStream gis = new GZIPInputStream(connection.getInputStream());
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(gis, StandardCharsets.UTF_8));
String inputLine;
StringBuilder stringBuilder = new StringBuilder();
while ((inputLine = bufferedReader.readLine()) != null) {
stringBuilder.append((inputLine));
}
bufferedReader.close();
return gson.fromJson(stringBuilder.toString(), Root.class);
}
}
Второй:
public class StackOverFlowServlet extends HttpServlet {
private String tag;
private Gson gson;
private Root root;
public void setParameter(String tag, Gson gson, Root root){
this.tag = tag;
this.gson = gson;
this.root = root;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json");
try {
if (tag == null) {
resp.setStatus(404);
} else {
List<Boolean> isAnswered = new ArrayList<>();
for (Item root1 : root.items) {
if (root1.is_answered) {
isAnswered.add(true);
}
}
resp.getWriter().printf("\"%s\":{ \"total:\": %s, \"answered\": %s}\n", tag, gson.toJson(root.items.size()), isAnswered.size());
}
} catch (Exception e){
e.printStackTrace();
Error error = new Error(String.format("Kakaya-to huynya proizoshla: %s", e.getMessage()));
resp.getWriter().print(gson.toJson(error));
resp.setStatus(500);
}
}
}
Ну и два класса по котороым JSON парсит значения из API:
package json;
import java.util.List;
public class Root{
public List<Item> items;
public boolean has_more;
public int quota_max;
public int quota_remaining;
}
package json;
import java.util.List;
public class Item {
public List<String> tags;
public boolean is_answered;
public int view_count;
public int answer_count;
public int score;
public int last_activity_date;
public int creation_date;
public int question_id;
public String content_license;
public String link;
public String title;
public int closed_date;
public String closed_reason;
public int last_edit_date;
public int accepted_answer_id;
}
Ответы (1 шт):
Неправильно используется API для асинхронной обработки запроса. Главная проблема, что AsyncContext.start и AsyncContext.complete при обработке запроса можно и нужно вызывать ровно один раз. То есть это API позволяет выполнить обработку запроса асинхронно в отдельном потоке, но не дает возможность распараллелить выполнение, это нужно делать самому.
Вторая вытекающая из этого проблема это запись в один поток вывода ответа (resp.getWriter().printf) из разных потоков выполнения. Нет никакой гарантии, что каждый из потоков выполнения полностью запишет свои данные до того как другой запишет свои. То есть данные в ответ могут попасть вперемешку от разных потоков, даже в середине строки, которую записывает один поток.
Еще одна проблема это то, что в асинхронном обработчике идет обращение к HttpServletRequest и HttpServletResponse, которые были переданы в doGet. Это неправильно, их нужно брать из AsyncContext.
То есть решение должно стартовать потоки для обработки отдельных тегов, а асинхронный обработчик должен ждать, когда каждый из запущенных потоков закончит выполнение и уже сам записать результат в ответ. Вот схема решения:
public class AsyncServer extends HttpServlet {
private ExecutorService executorService = Executors.newFixedThreadPool(10);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
...
ctxt.start(new Runnable() {
@Override
public void run() {
try {
List<Future<Root>> futures = new ArryaList<>();
for (String tag: tags) {
// добавляем задачу по обработке тега
// это Callable возвращающий объект Root
futures.add(
executorService.submit(() -> getStackOverFlow(tag));
);
}
Writer writer = ctxt.getResponse().getWriter();
for (Future<Root> future : futures) {
// future.get ждет пока поток обрабатывающий тег закончится
// и возвращает результат
Root root = future.get();
// тут записываем кусок json соответствующий данным из root
// используя writer.printf();
}
ctxt.complete();
} catch (ServletException | IOException e) {
e.printStackTrace();
}
}
}
}