Это небольшое дополнение к моему предыдущему сообщению «Spring Security и неплоская архитектура наследования ролей». В этой статье я говорю вам:

  1. Как создать документацию для правил авторизации Spring Security непосредственно из кода?
  2. Как разместить полученную HTML-страницу на GitHub Pages?

Такая документация полезна для самых разных специалистов. Системные и бизнес-аналитики хотят понимать логику обработки запросов. В то время как инженеры по качеству проверяют, чтобы конечные точки проверяли доступ, как описано в задаче. Обе эти категории выиграют от документации, которая всегда актуальна.

Весь код генератора вы можете найти по этой ссылке. Посмотрите на пример сгенерированной документации ниже.

Вы можете просмотреть отрендеренный HTML по этой ссылке.

Алгоритм

Вот вся идея генерации документации:

  1. Существует отдельный DocsTest, который запускает все приложение Spring.
  2. Во время выполнения теста создается результирующая HTML-страница в каталоге build/classes/test.
  3. Наконец, во время выполнения конвейера GitHub мы размещаем выходную HTML-страницу на GitHub Pages.

Шаги генерации

Взгляните на базовую настройку ниже.

class DocsTest extends AbstractControllerTest {
    @Autowired
    private ApplicationContext context;
    ...
}

Команда AbstractControllerTest запускает PostgreSQL с тестовыми контейнерами. Вы можете найти его исходный код по этой ссылке.

Я использую bean-компонент ApplicationContext для разрешения зарегистрированных контроллеров REST.

Какую информацию нам нужно извлечь из аннотаций, размещенных на контроллере REST? Вот список:

  1. Имя контроллера
  2. Подробная информация о каждой конечной точке:
  3. HTTP-метод
  4. Путь к API
  5. Выражение безопасности SpEL, проанализированное из аннотации @PreAuthorize.
  6. Имя метода Java, отображающего HTTP-запрос.

Посмотрите на записи Java, которые содержат указанные точки:

@With
private record ControllerInfo(
    String name,
    List<MethodInfo> methods
) {}

@With
private record MethodInfo(
    String httpMethod,
    String apiPath,
    String security,
    String functionName
) {}

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

@Test
void generateDocs() throws Exception {
    final var controllers = new ArrayList<ControllerInfo>();
    for (String controllerName : context.getBeanNamesForAnnotation(RestController.class)) {
        final var controllerBean = context.getBean(controllerName);
        final var baseApiPath = getApiPath(AnnotationUtils.findAnnotation(controllerBean.getClass(), RequestMapping.class));
        final var controllerSecurityInfo = new ControllerInfo(
            StringUtils.capitalize(controllerName),
            new ArrayList<>()
        );
        for (Method method : controllerBean.getClass().getMethods()) {
            getMethodInfo(method)
                .map(m -> m.withPrefixedApiPath(baseApiPath))
                .ifPresent(m -> controllerSecurityInfo.methods().add(m));
        }
        controllers.add(controllerSecurityInfo);
    }
    ...
}

Вот что происходит шаг за шагом:

  1. Я извлекаю все имена bean-компонентов, помеченные аннотацией @RestController.
  2. Затем я получаю текущий компонент контроллера по его имени.
  3. После этого я анализирую базовый путь API.
  4. И, наконец, я просматриваю каждый метод внутри контроллера и анализирую информацию о нем.

Посмотрите на декларацию getMethodInfo ниже.

private static Optional<MethodInfo> getMethodInfo(Method method) {
        return Optional.<Annotation>ofNullable(AnnotationUtils.findAnnotation(method, GetMapping.class))
                   .or(() -> ofNullable(AnnotationUtils.findAnnotation(method, PostMapping.class)))
                   .or(() -> ofNullable(AnnotationUtils.findAnnotation(method, DeleteMapping.class)))
                   .or(() -> ofNullable(AnnotationUtils.findAnnotation(method, PutMapping.class)))
                   .map(annotation -> AnnotationUtils.getAnnotationAttributes(method, annotation))
                   .map(attributes -> new MethodInfo(
                       attributes.annotationType()
                           .getSimpleName()
                           .replace("Mapping", "")
                           .toUpperCase(),
                       getApiPath(attributes.getStringArray("value")),
                       ofNullable(AnnotationUtils.findAnnotation(method, PreAuthorize.class))
                           .map(PreAuthorize::value)
                           .orElse(""),
                       method.getName()
                   ));
    }

В этом случае я пытаюсь получить возможные аннотации сопоставления запросов из метода: GetMapping, PostMapping, DeleteMapping или PutMapping. Затем я получаю атрибуты аннотации, вызывая AnnotationUtils.getAnnotationAttributes, и, наконец, передаю параметры конструктору MethodInfo.

Метод getApiPath принимает параметр String... и возвращает его первое значение, если оно присутствует.

Создание HTML-отчета

Теперь, когда у нас есть информация о конечных точках, пришло время отформатировать ее как HTML-страницу. Посмотрите на объявление шаблона ниже:

final var html = """
    <html>
    <head>
        <meta charset="UTF8">
        <style>
            body, table {
                font-family: "JetBrains Mono";
                font-size: 20px;
            }
            table, th, td {
              border: 1px solid black;
            }
        </style>
        <link href='https://fonts.googleapis.com/css?family=JetBrains Mono' rel='stylesheet'>
    </head>
    <body>
        <div>
            <h2>Endpoints role checking</h2>
            <div>{docs}</div>
        </div>
    </body>
    </html>
    """.replace("{docs}", toHtml(controllers));

writeFileToBuildFolder("index.html", html);

Переменная controllers представляет List<ControllerInfo>, которую мы построили ранее. Функция toHtml преобразует его во фрагмент HTML. Затем мы заменяем заполнитель {docs} содержимым.

Функция writeFileToBuildFolder записывает содержимое результата в файл build/classes/java/test/index.html. С его объявлением вы можете ознакомиться по этой ссылке.

Посмотрите на определение функции toHtml ниже.

private static String toHtml(List<ControllerInfo> controllers) {
    StringBuilder docs = new StringBuilder();
    for (ControllerInfo controller : controllers) {
        docs.append("<b>")
            .append(controller.name())
            .append("</b>")
            .append("<br>")
            .append("<table>");

        for (MethodInfo method : controller.methods()) {
            docs.append("<tr>")
                .append("<td>").append(method.httpMethod()).append("</td>")
                .append("<td>").append(method.apiPath()).append("</td>")
                .append("<td>").append(method.security()).append("</td>")
                .append("<td>").append(method.functionName()).append("</td>")
                .append("</tr>");
        }
        docs.append("</table>")
            .append("----------------------------------<br>");
        }
    return docs.toString();
}

Как видите, я просто создаю HTML-таблицу для каждого существующего контроллера и объединяю их в одну строку.

Размещение документации на GitHub Pages

Весь конвейер GitHub Actions состоит менее чем из 40 строк. Посмотрите на YAML ниже.

name: Java CI with Gradle

on:
  push:
    branches: [ "master" ]

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build-and-deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      - name: Build with Gradle
        run: ./gradlew build
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v1
        with:
          path: build/classes/java/test/
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v1

Вот что происходит:

  1. Set up JDK 17 и Build with Gradle выполняют обычную операцию сборки Gradle.
  2. Затем идет Upload artifact, который сохраняет каталог, содержащий HTML-документацию, в реестре GitHub.
  3. Наконец, мы развертываем ранее сохраненный артефакт на страницах GitHub.

И это в основном все. Вы можете проверить сгенерированную HTML-страницу по этой ссылке. Самое крутое, что не надо писать документацию вручную. Поэтому это всегда актуально, потому что вы генерируете контент непосредственно из своего кода.

Заключение

Это все, что я хотел вам рассказать о документировании приложений Spring Security и сохранении HTML-результата на страницах GitHub. Вы создаете какие-либо документы в своих проектах? Если да, то что это за документация? Расскажите свою историю в комментариях. Спасибо за прочтение!

Ресурсы

  1. Мой предыдущий пост 'Spring Security и неплоская архитектура наследования ролей'
  2. Гитхаб-страницы
  3. Весь код генератора
  4. Визуализированная HTML-страница, размещенная на GitHub Pages
  5. AbstractControllerTest с настройкой Testcontainers
  6. Грэдл