﻿# Сердце облачной Java: проверяем Quarkus

Java уже какое\-то время витает в облаках\. Всё больше приложений и сервисов переходят на облачную архитектуру\. Мы решили не отставать и проверить один из главных проектов для разработки производительных облачных приложений на Java — Quarkus\.

![1398_Quarkus_ru/image1.png](https://import.viva64.com/docx/blog/1398_Quarkus_ru/image1.png)

## Введение

Мы регулярно [проверяем](https://pvs-studio.ru/ru/blog/inspections/) Open Source проекты с помощью нашего анализатора PVS\-Studio и публикуем статьи о найденных ошибках\. Такие статьи позволяют читателям ближе познакомиться с концепцией статического анализа, запомнить, какие ошибки случаются чаще всего, а также разобраться в нюансах и тонкостях при разработке на определённом языке\. 

Особенно интересно смотреть на ошибки в крупных проектах, ведь именно они постоянно развиваются: у них огромная кодовая база, а при их разработке применяются разные подходы и технологии\. Это значит, что и сами ошибки могут быть совершенно разными\. 

В этот раз выбор упал на популярный проект [Quarkus](https://github.com/quarkusio/quarkus)\. Это современный фреймворк, ориентированный на разработку высокопроизводительных облачных приложений на Java с использованием технологий, ставших стандартами в корпоративной разработке \(микросервисы, DI\-контейнеры и так далее\)\. 

Quarkus делает упор на производительности готовых приложений за счёт [смещения большого количества операций на время сборки](https://quarkus.io/performance/): чтение конфигураций, сканирование classpath, построение контекста приложения\. Также приложения на Quarkus удобно компилируются в нативный образ\. 

Из этого понятно, что перед нами проект непростой и большой\. Поэтому мы решили посмотреть, какие ошибки в нём удастся найти\. За основу для проверки взяли вот этот [коммит](https://github.com/quarkusio/quarkus/tree/19d6bc987d61c3d70995b557a93e840a1106bef0)\. Не будем больше задерживаться, давайте переходить к делу\. 

## Трудный ребёнок и как с ним справиться

Вы подумали, что я буду рассказывать лекции о воспитании? Я всего лишь напомню, какие подводные камни бывают в иерархии наследования Java\. Предлагаю посмотреть на такой код\.

**Фрагмент 1**

```cpp
public class InterceptorInfo extends BeanInfo 
                             implements Comparable<InterceptorInfo> {
  private final Set<AnnotationInstance> bindings;
  .... 
  @Override
  public String toString() {
    return "INTERCEPTOR bean [bindings=" + bindings + 
           ", target=" + getTarget() + "]"; 
  }
}
```

Обратите внимание, что в методе `toString` используется поле `bindings` класса `InterceptorInfo`\. А теперь посмотрим на конструктор родительского класса `BeanInfo`:

```cpp
BeanInfo(....) {
  ....
  this.identifier = Hashes.sha1_base64(
            (identifier != null ? identifier : "") + 
            toString() + beanDeployment.toString()
           );
  ....
}
```

И чтобы окончательно сложить весь паззл в одну картину, вот конструктор класса `InterceptorInfo`:

```cpp
InterceptorInfo(. . . ., Set<AnnotationInstance> bindings) {
  super(. . . .);
  this.bindings = bindings;
  ....
}
```

Итак, давайте разбираться, что здесь происходит\. При инициализации класса `InterceptorInfo` сначала вызывается конструктор родительского класса `BeanInfo`\. Тот, в свою очередь, вызывает метод `toString`, который переопределён в классе `InterceptorInfo`\. Метод `toString` использует не инициализированное поле `bindings`\. В результате полученная строка отображает не до конца сформированное состояние объекта\. 

Подробнее об этой проблеме и как с ней бороться мы писали в отдельной [статье](https://pvs-studio.ru/ru/blog/posts/java/1132/)\. 

Предупреждение PVS\-Studio:

[V6052](https://pvs-studio.ru/ru/docs/warnings/v6052/) Calling overridden 'toString' method in 'BeanInfo' parent\-class constructor may lead to use of uninitialized data\. Inspect field: bindings\. [InterceptorInfo\.java 265](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java#L266)

## Потеряшки

Забытые, потерянные или перепутанные фрагменты кода частенько появляются в том или ином проекте, и эта ошибка не обошла стороной Quarkus\. 

**Фрагмент 2**

```cpp
public void build(Path projectDir) {
  .... 
  try {
    ModelUtils.persistModel(projectDir.resolve("pom.xml"), model);
  } catch (IOException e) {
    throw new IllegalStateException();   // <= 
  }
}
```

Этот метод класса отвечает за построение модели проекта Maven и его зависимостей\. В показанном фрагменте видно, что при сохранении файла модели может возникнуть исключение, информацию о котором перетирают исключением типа `IllegalStateException`\. Это не самая лучшая практика, так как в случае возникновения проблем, исключение, брошенное из метода `build`, не будет содержать информации о реальной ошибке\. Никто не узнает, какие скелеты спрятались в вашем шкафу\. 

Предупреждение PVS\-Studio:

[V6118](https://pvs-studio.ru/ru/docs/warnings/v6118/) The original exception object 'IOException' was swallowed\. Cause of original exception could be lost\. [MvnProjectBuilder\.java 122](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/independent-projects/bootstrap/maven-resolver/src/test/java/io/quarkus/bootstrap/workspace/test/MvnProjectBuilder.java#L123)

**Фрагмент 3**

```cpp
Foo.Bar bar1 = new Bar(new ArrayList<>());
event1.fire(bar1);
assertEquals(1, bar1.getNames().size());     // <= 
assertEquals("bazinga", bar1.getNames().get(0));

Foo.Bar bar2 = new Bar(new ArrayList<>());
event2.fire(bar2);
assertEquals(1, bar1.getNames().size());     // <= 
assertEquals("bazinga", bar2.getNames().get(0));
```

Это фрагмент из тестов, проверяющих механизм событий\. Здесь создают и тестируют два экземпляра `Foo.Bar`\. Нетрудно заметить, что второй блок тестового кода — копипаста первого\. В продублированной строке `assertEquals(1, bar1.getNames().size())` забыли заменить `bar1` на `bar2`, из\-за чего проверяют состояние не того объекта\. 

Предупреждение PVS\-Studio:

[V6072](https://pvs-studio.ru/ru/docs/warnings/v6072/) Two similar code fragments were found\. Perhaps, this is a typo and 'bar2' variable should be used instead of 'bar1'\. [MockEventTest\.java](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/MockEventTest.java#L47) 40

Кстати, мы добавили точно такую же [диагностику](https://pvs-studio.ru/ru/docs/warnings/v7023/) в наш новый анализатор для JavaScript и TypeScript\. 

**Фрагмент 4**

```cpp
public class PathTreeBuilder {
  private List<String> includes;
  private List<String> excludes;
  ....
  List<String> getIncludes() {
    return includes;
  }
  
  List<String> getExcludes() {
    return includes;       // <= 
  }
}
```

Как можно догадаться из названия класса, `PathTreeBuilder` создаёт экземпляр `PathTree`, который нужен для удобной работы с зависимостями проекта в виде дерева\. Строитель имеет методы заполнения полей `includes` и `excludes`\. Однако в методе `getExcludes` по ошибке возвращают поле `includes`\. 

Предупреждение PVS\-Studio:

[V6091](https://pvs-studio.ru/ru/docs/warnings/v6091/) Suspicious getter implementation\. The 'excludes' field should probably be returned instead\. [PathTreeBuilder\.java](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTreeBuilder.java#L59) 58

**Фрагмент 5**

```cpp
private static boolean isIgnored(DotName classDotName) {
  String className = classDotName.toString();
  if (className.startsWith("java.util.") 
      || className.startsWith("java.lang.")
      || className.startsWith("org.hibernate.engine.spi.")
      || className.startsWith("jakarta.persistence.")
      || className.startsWith("jakarta.persistence.")    // <=
  ) {
    return true;
  }
  return false;
}
```

Этот метод используется в классе `JpaJandexScavenger` для фильтрации классов, необходимых в работе интегрированного Hibernate ORM\. Сам же класс заранее определяет, какие для этого типы нужны\. Как уже было сказано во введении, Quarkus старается перенести часть логики на момент сборки и `JpaJandexScavenger` реализует именно эту концепцию\. 

В теле метода можно увидеть повторение пакета `jakarta.persistence.` в логическом условии:

```cpp
className.startsWith("jakarta.persistence.") ||
className.startsWith("jakarta.persistence.")
```

Учитывая назначение этого метода, не исключено, что здесь забыли добавить ещё какое\-то ограничение\. На функционал это может и не повлиять, однако размер итогового приложения может вырасти за счёт покрытия большего числа классов метаданными\. А может, всё гораздо проще, и это просто лишняя проверка, оставленная по ошибке\. 

Предупреждение PVS\-Studio:

[V6001](https://pvs-studio.ru/ru/docs/warnings/v6001/) There are identical sub\-expressions 'className\.startsWith\("jakarta\.persistence\."\)' to the left and to the right of the '\|\|' operator\. [JpaJandexScavenger\.java 621](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/JpaJandexScavenger.java#L622)

## Просчитались, но где?

Работа с числами — ещё одно пространство для ошибок\. Давайте разбираться\.

**Фрагмент 6**

```cpp
@ConsumeEvent("address-4")
CompletionStage<Long> listenAddress4(int i) {
  return CompletableFuture.completedFuture((long) (i + 1));
}
```

Если переменная `i` будет равна `Integer.MAX_VALUE`, то при добавлении к ней единицы произойдёт переполнение, и результат этой операции станет равен `Integer.MIN_VALUE`\. Защититься от такой ошибки можно путём приведения одного из операндов к `long`\. Хотя в этом примере приведение есть, его применяют к результату операции, так что ошибки избежать не удастся\. 

Предупреждение PVS\-Studio:

[V6117](https://pvs-studio.ru/ru/docs/warnings/v6117/) Possible overflow\. The expression will be evaluated before casting\. Consider casting one of the operands instead\. [CodecRegistrationTest\.java 195](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/CodecRegistrationTest.java#L195)

**Фрагмент 7**

```cpp
@Override
public void apply(SocketSettings.Builder builder) {
  if (config.connectTimeout().isPresent()) {
    builder.connectTimeout((int) config.connectTimeout().get()       // <= 
                                       .toMillis(), TimeUnit.MILLISECONDS); 
  }
  if (config.readTimeout().isPresent()) {
    builder.readTimeout((int) config.readTimeout().get()        // <=
                                    .toMillis(), TimeUnit.MILLISECONDS);
  }
}
```

Ещё один пример приведения, который может привести к переполнению\. Его цель не ясна, тем более что и `connectTimeout`, и `readTimeout` у переменной `builder` принимают `long`\. 

Анализатор PVS\-Studio выдаёт следующие предупреждения в этих местах:

[V6106](https://pvs-studio.ru/ru/docs/warnings/v6106/) Casting expression to int type before implicitly casting it to other type may be excessive or incorrect\. [MongoClients\.java 289](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClients.java#L289)

[V6106](https://pvs-studio.ru/ru/docs/warnings/v6106/) Casting expression to int type before implicitly casting it to other type may be excessive or incorrect\. [MongoClients\.java 286](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClients.java#L286)

**Фрагмент 8**

```cpp
static <T> List<T> takeLast(List<T> list, int n) {
  if (n < 1 || n > list.size()) {
    throw new IndexOutOfBoundsException(n);
  }
  if (list.isEmpty()) {   // <= 
    return list;
  }
  return list.subList(list.size() - n, list.size());
}
```

Метод берёт последние `n` элементов из `list`\. Подразумевается, что нельзя взять меньше одного элемента или больше, чем в этом списке есть\. Но проверка говорит ещё и о том, что сам список быть пустым не может\. Если мы берём 1 элемент из списка, это значит, что он должен иметь как минимум один элемент\. Поэтому следующая проверка `list.isEmpty()` всегда ложная\. Если автор кода хотел обработать случай взаимодействия с пустым списком, то тут этого сделать не выйдет, и текущую проверку стоит пересмотреть\.

Предупреждение PVS\-Studio:

[V6007](https://pvs-studio.ru/ru/docs/warnings/v6007/) Expression 'list\.isEmpty\(\)' is always false\. [CollectionTemplateExtensions\.java 57](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/CollectionTemplateExtensions.java#L57)

**Фрагмент 9**

```cpp
if (errorRate != -1.0) {
  list.add("ERROR");
  list.add(new BigDecimal(errorRate).toPlainString()); // Prevent E notation
}
```

С этой ошибкой предлагаю ознакомиться подробнее\. Если мы передадим в конструктор `BigDecimal` число `0.1`, то будет сохранено `0.1000000000000000055511151231257827021181583404541015625`\. Наличие такого хвоста — особенность представления вещественных чисел в компьютере\. Для тех, кто интересуется, почему так происходит, оставлю здесь [ссылку](https://ru.wikipedia.org/wiki/IEEE_754) на теоретическую информацию\. 

Ну а нас теперь разберёмся, как можно создать объект без "сюрприза" в виде хвоста\. Для этого в стандартной библиотеке есть метод [`BigDecimal#valueOf`](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/math/BigDecimal.html#valueOf(double)), внутри которого реализована хитрая логика: 

```cpp
public static BigDecimal valueOf(double val) {
  .... 
  var fmt = FormattedFPDecimal.valueForDoubleToString(Math.abs(val));
  long s = fmt.getSignificand();
  .... 
}
```

Если говорить общими словами, то происходит следующее: метод получает двоичное представление вещественного числа и переводит его в десятичное представление так, чтобы в итоге мы получили корректно округлённое, кратчайшее по записи десятичное число\. В случае с вышерассмотренным числом таким строковым значением как раз будет 0\.1\. Поэтому метод [`BigDecimal#valueOf`](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/math/BigDecimal.html#valueOf(double)) сохранит наше точное значение\.

<details>
   <summary>Но так было не всегда\\\.\\\.\\\.</summary>

Впервые этот метод появился в Java 5 и выглядел совсем иначе: 

```cpp
public static BigDecimal valueOf(double val) { 
  return new BigDecimal(Double.toString(val));
}
```



И хотя вместо десятичного представления здесь использовалась строка, глобально задача решалась точно так же\. 

К слову, метод [`BigDecimal#valueOf`](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/math/BigDecimal.html#valueOf(double)) до сих пор в своей Javadoc ссылается на [`Double#toString()`](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Double.html#toString(double)), хотя сейчас под капотом использует другие методы\. Но суть алгоритма осталась прежней, и она хорошо описана в Javadoc к методу [`Double#toString()`](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Double.html#toString(double))\. 



Ну а что было до Java 5? Все просто: конструктор `BigDecimal(String)` существовал всегда и рекомендовался к использованию вместо `BigDecimal(double)`\.


</details>


Подобные тонкости имеют особое значение в системах точных вычислений, где даже небольшие погрешности могут привести к неожиданным последствиям\.

Предупреждение PVS\-Studio:

[V6068](https://pvs-studio.ru/ru/docs/warnings/v6068/) Constructor call can result in imprecise representation of the initialized value\. [BfInsertArgs\.java 95](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfInsertArgs.java#L95)

## Новогодний переполох

Если после празднования Нового Года вы оказались в прошлом или будущем, не пугайтесь — это баг\.

**Фрагмент 10**

```cpp
public class WebSocketNextJsonRPCService implements ConnectionListener {
  private static final DateTimeFormatter FORMATTER = 
    DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss");  
  .... 
}
```

На первый взгляд здесь вообще никакой проблемы нет, но давайте посмотрим внимательно на строку формата даты: `YYYY-MM-dd`\. И снова выглядит всё нормально\. Или нет? 

Если текущая дата, к примеру, 1\.1\.2027, то с таким форматом отображаться она будет как 1\.1\.2026\.

Всё из\-за того, что такой формат записи с `YYYY` отображает год на основе номера недели в нём\. И для вычисления года есть даже хитрая схема: если большая часть недели принадлежит предыдущему году, то отображаться будет именно он\. В нашем примере 1 января — пятница, а значит, большая часть недели принадлежит 2026 году\. 

Обратная ситуация работает, если меньшая часть недели принадлежит уходящему году\. Так, 31\.12\.2024 по этому формату будет отображаться как 31\.12\.2025, потому что большая часть недели принадлежит 2025 году\. Незаметно улетел целый год\.\.\. 

Это настолько необычная ошибка, что мы посвятили ей отдельную [статью](https://pvs-studio.ru/ru/blog/posts/java/1185/)\. 

А исправить её можно заменой `YYYY` на `yyyy`, о чём так же предупреждает PVS\-Studio:

[V6122](https://pvs-studio.ru/ru/docs/warnings/V6122/) Usage of 'Y' \(week year\) pattern was detected: it was probably intended to use 'y' \(year\)\. [WebSocketNextJsonRPCService\.java 39](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/websockets-next/runtime-dev/src/main/java/io/quarkus/websockets/next/runtime/dev/ui/WebSocketNextJsonRPCService.java#L39)

## Теория разбитых окон

Что за теория такая и как она связана с разработкой, мы писали в отдельной [статье](https://pvs-studio.ru/ru/blog/posts/1237/#IDED1685F479), а сейчас мы посмотрим наглядно на то, как она себя проявляет\. 

**Фрагмент 11**

```cpp
@Override
public <T> T getClaim(String claimName) {
  if (claimName.equals(Claims.groups)) {
    return (T) getGroups();
  } else if (claimName.equals(Claims.groups)) {  // <=
    return (T) getAudience();
  } else if (claimName.equals(Claims.exp)) {
    return (T) Long.valueOf(getExpirationTime());
  } else if (claimName.equals(Claims.iat)) {
    return (T) Long.valueOf(getIssuedAtTime());
  } else if (claimName.equals(Claims.aud)) {
    return (T) getAudience();
  }
  return (T) claims.getClaim(claimName);
}
```

Предупреждение PVS\-Studio:

[V6003](https://pvs-studio.ru/ru/docs/warnings/v6003/) The use of 'if \(A\) \{\.\.\.\} else if \(A\) \{\.\.\.\}' pattern was detected\. There is a probability of logical error presence\. [CognitoPrincipal\.java 38](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java#L38) 

Что мы тут видим: во втором условии цепочки `if-else` допустили опечатку и ещё раз проверяют на равенство с `Claims.groups`, при том, что возвращают результат вызова метода `getAudience`\. Можно сказать, что ничего страшного, ведь правильное условие описано последним в цепочке `if-else`, а вторая проверка `claimName.equals(Claims.groups)` уже не будет истинной, ведь она не прошла в первом условии\. Но здесь важно не это\. Это срабатывание из пакета `amazon-lambda-rest`\. Теперь предлагаю посмотреть на другое срабатывание рядом, в пакете `amazon-lambda-http`:

```cpp
@Override
public <T> T getClaim(String claimName) {
  if (claimName.equals(Claims.groups)) {
    return (T) getGroups();
  } else if (claimName.equals(Claims.groups)) {  // <=
    return (T) getAudience();
  } else if (claimName.equals(Claims.exp)) {
    return (T) Long.valueOf(getExpirationTime());
  } else if (claimName.equals(Claims.iat)) {
    return (T) Long.valueOf(getIssuedAtTime());
  } else if (claimName.equals(Claims.aud)) {
    return (T) getAudience();
  }
  return (T) getClaims().getClaims().get(claimName);
}
```

И снова точно такой же код и точно такая же ошибка\. Разница лишь в последней строке\. Вот так ошибочный код стал расползаться по проекту через обычный copy\-paste\. 

Срабатывание PVS\-Studio во втором пакете: [V6003](https://pvs-studio.ru/ru/docs/warnings/v6003/) The use of 'if \(A\) \{\.\.\.\} else if \(A\) \{\.\.\.\}' pattern was detected\. There is a probability of logical error presence\. [CognitoPrincipal\.java 41](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java#L41)

**Фрагмент 12**

```cpp
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
VertxWebRouterBuildItem initializeRouter(....) {
  .... 
  List<RouteBuildItem> redirectRoutes = new ArrayList<>();
  .... 
  if (frameworkRouterCreated) {    
    if (redirectRoutes.size() > 0) {    // <= 
      recorder.setNonApplicationRedirectHandler(
        nonApplicationRootPath.getNonApplicationRootPath(),
        nonApplicationRootPath.getNormalizedHttpRootPath()
      );
      redirectRoutes.forEach(route -> recorder.addRoute(
                           httpRouteRouter, 
                           route.getRouteFunction(),
                           recorder.getNonApplicationRedirectHandler(),
                           route.getType()
                          )
      );
    }
  }
  return new VertxWebRouterBuildItem(httpRouteRouter, mainRouter, 
                                     frameworkRouter, managementRouter,
                                     mutinyRouter);
}
```

Предупреждение PVS\-Studio:

[V6007](https://pvs-studio.ru/ru/docs/warnings/v6007/) Expression 'redirectRoutes\.size\(\) \> 0' is always false\. [VertxHttpProcessor\.java 392](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java#L392)

Тут мы видим, что часть логики никогда не выполняется, потому что коллекция `redirectRoutes` ничем не заполняется в рамках метода и никуда не передаётся\. Мы решили разобраться и посмотреть историю проекта на этом файле\. В итоге удалось найти [коммит](https://github.com/quarkusio/quarkus/commit/e94ac2b8a2c973b81a23498b288fba4f6df7e76a#diff-f15233cea0f87118b4441038738a41197a55fb6e3e1f3e06efd1dc4b41fe0143) 2021 года с интересными удалёнными строками: 

```cpp
if (httpBuildTimeConfig.redirectToNonApplicationRootPath && 
    route.isRequiresLegacyRedirect()
) {
  redirectRoutes.add(route);
}
```

А причём тут разбитые окна? На самом деле всё просто: лишний код, оставленный в одном месте, может привести к появлению такого же кода в других частях проекта\. Например, в забытом фрагменте используется метод, который не удаляют, потому что он всё ещё нужен именно там\. В итоге забытый код цепляет уже два класса, и эту цепочку можно продолжать долго\. Как результат — раздутая кодовая база и трудности в восприятия всего кода\.

## Не доверяй переменным среды

Вот такой код подсветил анализатор\. 

**Фрагмент 13**

```cpp
static final boolean IS_CYGWIN = OS.WINDOWS.isCurrent()
        && System.getenv("PWD") != null
        && System.getenv("PWD").startsWith("/");
```

Здесь обращаются к переменной окружения `PWD`\. Обычно так обозначают рабочую директорию процесса\. С ней есть свои заморочки: она может быть или не заданной, или переопределённой извне\. Если хочется стабильных результатов, то безопаснее использовать системное свойство JVM [`user.dir`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/System.html#user.dir), поскольку оно гарантированно выставляется при старте приложения и извне не меняется\.

Предупреждения PVS\-Studio на этот код:

[V6110](https://pvs-studio.ru/ru/docs/warnings/v6110/) Using the 'PWD' environment variable could be unsafe or unreliable\. Consider using trusted system property 'user\.dir' instead\. [TerminalUtils\.java 27](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/core/devmode-spi/src/main/java/io/quarkus/dev/console/TerminalUtils.java#L27)

[V6110](https://pvs-studio.ru/ru/docs/warnings/v6110/) Using the 'PWD' environment variable could be unsafe or unreliable\. Consider using trusted system property 'user\.dir' instead\. [TerminalUtils\.java 28](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/core/devmode-spi/src/main/java/io/quarkus/dev/console/TerminalUtils.java#L28)

Кроме того, в проекте есть ещё несколько подобных срабатываний, но уже с другими значениями, которые также рекомендуется получать через вызов `System.getProperty`: 

[V6110](https://pvs-studio.ru/ru/docs/warnings/v6110/) Using the 'HOME' environment variable could be unsafe or unreliable\. Consider using trusted system property 'user\.home' instead\. [Constants\.java 11](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/Constants.java#L11)

[V6110](https://pvs-studio.ru/ru/docs/warnings/v6110/) Using the 'USER' environment variable could be unsafe or unreliable\. Consider using trusted system property 'user\.name' instead\. [AnalyticsService\.java 231](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/independent-projects/tools/analytics-common/src/main/java/io/quarkus/analytics/AnalyticsService.java#L231)

## Горе от многопоточного кода

С многопоточным кодом нужно быть особенно внимательным, иначе беды не избежать\.

**Фрагмент 14**

```cpp
public class KubernetesDevUIProcessor {
  static volatile List<Manifest> manifests;
  public List<Manifest> getManifests() throws BootstrapException {
    if (manifests == null) {
      synchronized (Holder.class) {
        if (manifests == null) {
          manifests = new ArrayList<>();
          .... 
          try (CuratedApplication bootstrap = quarkusBootstrap.bootstrap()) {
            .... 
            for (var entry : context.entrySet()) {
              manifests.add(
                             new Manifest(entry.getKey(), 
                             new String(entry.getValue()))
                            );
            }
          }
        }
      }
    }
    return manifests;
  }
}
```

Это пример неправильной реализации паттерна double\-checked locking \(если интересно, как работает и зачем нужен этот паттерн, можно почитать нашу [статью](https://pvs-studio.ru/ru/blog/posts/java/1128/#ID1BD1BA8916)\) для поля `manifests`\. 

Когда поток заходит в блок синхронизации, он инициализирует поле `manifests` пустым списком, а потом переходит к процессу его заполнения\. То есть изначально предполагалось, что поле станет доступным после его создания и заполнения значениями\. Однако публикация происходит уже после инициализации `manifests`, и в результате другой поток может получить созданный, но ещё не заполненный список\. 

Исправить такую ошибку довольно просто: достаточно лишь инициализировать поле готовым списком из локальной переменной\.

Предупреждение PVS\-Studio:

[V6082](https://pvs-studio.ru/ru/docs/warnings/v6082/) Unsafe double\-checked locking\. Object was assigned to the field before it was initialized\. [KubernetesDevUIProcessor\.java 66](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/devui/KubernetesDevUIProcessor.java#L66)

**Фрагмент 15**

```cpp
public class VertxUdpMetrics implements DatagramSocketMetrics {
  private volatile Tags tags;

  @Override
  public void listening(String localName, SocketAddress localAddress) {
    tags = tags.and("address", NetworkMetrics.toString(localAddress));   // <= 
  }
}
```

Поле `tags` отметили ключевым словом `volatile`\. Вспомним, что это значит в Java: если один поток изменяет переменную, другой поток моментально видит это изменение\. Однако это работает безошибочно для атомарных операций\. Если нам нужно выполнить логику из нескольких действий, тут уже нужны совершенно другие подходы, чтобы избежать проблему [состояния гонки](https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5_%D0%B3%D0%BE%D0%BD%D0%BA%D0%B8)\.

Чтобы понять, что здесь происходит, представим, что метод `listening` одновременно используют два потока\. Оба зашли в метод, оба создали новый экземпляр через метод `and` и оба попытались сохранить новое значение в переменную `tags`\. 

Эти три операции не представляют собой одну атомарную, так что ключевое слово `volatile` тут не поможет и придётся использовать другие подходы \(например, делать метод синхронизированным\)\. 

Предупреждение PVS\-Studio:

[V6074](https://pvs-studio.ru/ru/docs/warnings/v6074/) Non\-atomic modification of volatile variable\. Inspect 'tags'\. [VertxUdpMetrics\.java 37](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxUdpMetrics.java#L37)

## Attention, please\!

Порой из\-за невнимательности рождается ошибочный или лишний код, как произошло здесь\.

**Фрагмент 16**

С этим фрагментом предлагаю разобраться поэтапно\. 

```cpp
public class TemplateHtmlBuilder {
  private static final String HEADER_TEMPLATE_NO_STACK = "<h1>%1$s</h1>\n" +
           "%2$s \n" +
           "<div class=\"container content\">\n";
  private static final String HTML_TEMPLATE_START_NO_STACK = "" +
             "<!doctype html>\n" +
             "<html lang=\"en\">\n" +
             "<head>\n" +
             "    <title>%1$s%2$s</title>\n" +
             "    <meta charset=\"utf-8\">\n" +
             "</head>";
 ....
}
```

Здесь мы видим два шаблона HTML, записанных в поля\. Если мы посмотрим на них внимательно, то заметим, что и в первом, и во втором шаблоне есть маркеры подстановки под две строковые переменные\. 

А теперь посмотрим на то, как они используются: 

```cpp
public TemplateHtmlBuilder(...., 
        String title, 
        String details
) {
  ....
  result = new StringBuilder(String.format(HTML_TEMPLATE_START_NO_STACK, 
                        escapeHtml(title),
                        subTitle == null || subTitle.isEmpty() ? "" : " - " + 
                        escapeHtml(subTitle), CSS));
  result.append(String.format(HEADER_TEMPLATE_NO_STACK, 
                escapeHtml(title),
                escapeHtml(details), 
                actionLinks.toString()
               ));
}
```

В использовании полей `HEADER_TEMPLATE_NO_STACK` и `HTML_TEMPLATE_START_NO_STACK` шаблон заполняется тремя аргументами в обоих случаях, хотя в самих строках мест всего два\. В этом же методе есть точно такие же [строки](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java#L321), которые используют те же самые аргументы, но с [шаблонами](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java#L134) для трёх аргументов\. Так что здесь мы видим пример обычного copy\-paste, когда забыли убрать лишние аргументы\. 

Предупреждение PVS\-Studio:

[V6046](https://pvs-studio.ru/ru/docs/warnings/V6046/) Incorrect format\. A different number of format items is expected\. Arguments not used: 3\. [TemplateHtmlBuilder\.java 325](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java#L325)

[V6046](https://pvs-studio.ru/ru/docs/warnings/V6046/) Incorrect format\. A different number of format items is expected\. Arguments not used: 3\. [TemplateHtmlBuilder\.java 328](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java#L328)



**Фрагмент 17**

```cpp
public void boot(...., Optional<FunctionInitializedBuildItem> hasFunctions) {
  if (!hasFunctions.isPresent() || hasFunctions.get() == null) // <= 
    return;
}
```

В метод приходит параметр `hasFunctions` типа `Optional`, с которым выполняют очень странную проверку:

```cpp
!hasFunctions.isPresent() || hasFunctions.get() == null
```

Напомним, что происходит внутри `Optional::isPresent`:

```cpp
public boolean isPresent() {
  return value != null;
}
```

Конкретно в этом случае нужно было оставить только вызов `Optional::isPresent`, так как `Optional::get` выбросит исключение, если значение действительно `null`\.

Предупреждение PVS\-Studio:

[V6007](https://pvs-studio.ru/ru/docs/warnings/v6007/) Expression 'hasFunctions\.get\(\) \=\= null' is always false\. [FunqyHttpBuildStep\.java 77](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/funqy/funqy-http/deployment/src/main/java/io/quarkus/funqy/deployment/bindings/http/FunqyHttpBuildStep.java#L77)

**Фрагмент 18**

```cpp
if (typeInfo == null || (typeInfo != null &&   
    typeInfo.endsWith(SectionHelperFactory.HINT_METADATA))
) {
  continue;
}
```

Здесь всё предельно просто: проверка `typeInfo != null` лишняя, так как обратная ситуация проверялась первой\. 

Предупреждение PVS\-Studio:

[V6007](https://pvs-studio.ru/ru/docs/warnings/v6007/) Expression 'typeInfo \!\= null' is always true\. [QuteProcessor\.java 920](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java#L920)

**Фрагмент 19**

```cpp
if (persistenceProviderResolver == null ||
     (persistenceProviderResolver != null       // <= 
      && !(persistenceProviderResolver instanceof
               MultiplePersistenceProviderResolver
           )
      )
) {
  ....
}
```

Это срабатывание похоже на предыдущее, но здесь даже проверка на равенство с `null` лишняя\. На самом деле, `instanceof` вернёт `false`, если передаваемый объект равен `null`\. Учитывая, что здесь проверяется, что `persistenceProviderResolver` не имеет тип `MultiplePersistenceProviderResolver`, столь громоздкое условие `if` можно сократить до: 

```cpp
!(persistenceProviderResolver instanceof MultiplePersistenceProviderResolver)
```

Предупреждение PVS\-Studio:

[V6007](https://pvs-studio.ru/ru/docs/warnings/v6007/) Expression 'persistenceProviderResolver \!\= null' is always true\. [PersistenceProviderSetup\.java 28](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceProviderSetup.java#L28)

**Фрагмент 20**

```cpp
for (String name : res.headers().names()) {
  if (name.equalsIgnoreCase("Transfer-Encoding")) {     // <= 
    continue; // ignore transfer encoding, 
            // chunked screws up message and response
  }
  for (String v : res.headers().getAll(name)) {
    if (name.equalsIgnoreCase("Transfer-Encoding")       // <=
        && v.contains("chunked")) { 
      continue;
    }
    responseBuilder.getMultiValueHeaders().add(name, v);
  }
}
```

Переменную `name` проверяют на равенство со строкой `Transfer-Encoding` перед внутренним циклом и на каждой его итерации\. Это лишняя операция, которая нагружает код и делает его менее компактным\. 

Предупреждение PVS\-Studio:

[V6007](https://pvs-studio.ru/ru/docs/warnings/v6007/) Expression 'name\.equalsIgnoreCase\("Transfer\-Encoding"\)' is always false\. [LambdaHttpHandler\.java 104](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java#L104)

**Фрагмент 21**

```cpp
while (
       !ResteasyReactiveDotNames.OBJECT.equals(currentClazz.name()) && 
       currentClazz != null              // <=
) { 
  ....
}
```

Ну и на десерт просто вишенка на торте: сначала разыменовывают `currentClazz` и лишь потом проверяют, что она не `null`\.

Предупреждение PVS\-Studio:

[V6060](https://pvs-studio.ru/ru/docs/warnings/v6060/) The 'currentClazz' reference was utilized before it was verified against null\. [ResteasyReactiveProcessor\.java 1125](https://github.com/quarkusio/quarkus/blob/19d6bc987d61c3d70995b557a93e840a1106bef0/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java#L1125)

## Заключение

Вот мы и рассмотрели интересные ошибки и подозрительные моменты, которые удалось найти в исходном коде Quarkus\. Мы проверяли другие крупные проекты и находили там немало интересного:

* [Операция "K"\. Ищем баги в коде IntelliJ IDEA](https://pvs-studio.ru/ru/blog/posts/java/1089/)
* [Поиск в поиске: проверка Elasticsearch](https://pvs-studio.ru/ru/blog/posts/java/1247/)
* [OpenAPI Generator через призму статического анализатора](https://pvs-studio.ru/ru/blog/posts/java/1344/)

Кстати, в репозитории Quarkus, как и во многих современных проектах, можно найти `.md` файлы для ИИ\-агентов\. Но насколько бы мощным инструментом ни был ИИ, даже он иногда совершает коварные ошибки — как, например, здесь: 

* [Давайте заглянем в этот самый вайб\-код](https://pvs-studio.ru/ru/blog/posts/cpp/1354/) 
* [Ревью вайб\-кода с гнильцой, который притворяется оптимизированным C\+\+ кодом](https://pvs-studio.ru/ru/blog/posts/cpp/1366/) 

А на этом мы эту статью завершаем\. Если вам интересно проверить свой проект, предлагаем пробную версию нашего анализатора\. Получить её можно [здесь](https://pvs-studio.ru/ru/pvs-studio/try-free/)\.