Вместо вступления


Если разбираешь что-то, попробуй это описать понятным языком и найти того, кто прочитает и выступит с замечаниями (перефразировал Р.Фейнмана, таки да я это сделал).
Все комментарии, даже злобные в стиле "Да, что этот ~белый~~чел. себе позволяет" приветствуются.


Цели


Приложения — отображение отчетов о ходе продукта (весы) по возможности с раздачей этих данных по сети в пределах предприятия (для функционала);
Личная — немного разобраться в технологии spring


Технологии


  • Spring Web
  • Spring JPA
  • Lombock
    • Thymeleaf
  • SpringFox Swagger (буду тестить рест на нем)
  • jaybird-jdk17, версия 3.0.5
  • Maven

Мотивация запилить spring+firebird


Недавно было сделано первое клиентское место под ОС Linux Mint для оператора "Овсезавода" и не всегда адекватная работа отображения отчетов из под Wine. (все остальное работает норм — визуализация Qt — SCADA, архивы Java SE).


Некоторые грабли на которые пришлось наступить


  1. jackson зависимости разных версий (исправлено),
  2. firebird не установленный тип кодировки ведет к default (ной) NONE,

Ссылка на git в конце публикации .


Jackson и все все все


Разные компоненты затянули jackson разных версий, как то неприятно, надо исправить.
Выявлено командной


mvn dependency:tree -Dincludes=com.fasterxml.jackson.core

+- org.springframework.boot:spring-boot-starter-web:jar:2.1.0.RELEASE:compile
[INFO] |  \- org.springframework.boot:spring-boot-starter-json:jar:2.1.0.RELEASE:compile
[INFO] |     \- com.fasterxml.jackson.core:jackson-databind:jar:2.9.7:compile
[INFO] \- io.springfox:springfox-swagger2:jar:2.7.0:compile
[INFO]    \- io.swagger:swagger-models:jar:1.5.13:compile
[INFO]       \- com.fasterxml.jackson.core:jackson-annotations:jar:2.8.5:compile

Исправляем в pom.


      <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.7</version>
            <exclusions>
                <exclusion>
                    <groupId>com.fasterxml.jackson.core</groupId>
                    <artifactId>jackson-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.fasterxml.jackson.core</groupId>
                    <artifactId>jackson-annotations</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.7</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.9.7</version>
        </dependency>

Подправил, ок. получилось. Если используете IDEA, то здесь еще проще посмотреть External Libraries. Что все зависимости есть и они нужных версий.


Общая структура приложения


С послойным тестированием Spring Boot приложения не знаком, поэтому сделаю без тестов
("Да что этот #цвет_кожи себе позволяет").


Конфигурация приложения


Поскольку я seasoned developer, а значит не ознакомлен с устоявшимися методами, то буду делать :


  1. в application.yml (пока настраиваю подключение к БД)
    spring:
    datasource:
    driver-class-name: org.firebirdsql.jdbc.FBDriver
    url: jdbc:firebirdsql://host:3050//work/cmn/db/namedb.fdb?sql_dialect=3&charSet=utf-8
    username: ******
    password: ******
    useUnicode: true
    characterEncoding: UTF-8
    sql-script-encoding: UTF-8
    jpa:
    show-sql: true
  2. используя аннотации непосредственно в классах:

Если не указать charSet=utf-8, то дефолтная будет NONE : На случай если в таблицах тоже NONE — получим нечитаемые символы или, согласно firebirdsql.org:


3.2.4 How can I solve the error “Connection rejected: No connection character set specified”
If no character set has been set, Jaybird 3.0 will reject the connection with an SQLNonTransientConnectionException with message “Connection rejected: No connection character set specified (property lc_ctype, encoding, charSet or localEncoding). Please specify a connection character set (eg property charSet=utf-8) or consult the Jaybird documentation for more information.”

Минимальный набор классов и файлов


Для начала index.html содержит пустое body;
Обкатка API — Swagger, package (infra) конфигурации которого поместим на уровне с остальными package проекта.



Добавлю в проект:


  • package model
    • clacc CModule — он же Data( getter, setter для членов, спасибо Lombok за минимум "code monkey"), он же Entity (сущность таблицы БД);
  • package repository
    • interface CModuleRepository extends JpaRepository<CModule,String> (он будет выбирать данные из базы), причем пока в него ничего добавлять не надо (типо Query);
  • package services;
    • class CModuleService — он же Service и @Transactional(readOnly = true) для работы с репозиторием;
  • package resources
    • class CModulesResource — он же @RestController, @RequestMapping("/modules") отвечать будет за обращение по этому адресу. Response body сделает сам (для нас выглядит так)

Будем работать с API RestController по всем путям, укажем это Swagger:


@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
    @Bean
    public Docket documentation(){
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
                .paths(PathSelectors.any())
                .build();
    }
}

Создадим класс запускающий Spring Application:


@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class,args);
    }
}

Модели, репозитории, request созданы для нужной таблицы (у меня это modules):
теперь можно браться за @RestController


@RestController
@RequestMapping("/modules")
@Api(tags = "Modules", description = "Modules API")
public class CModulesResource {
....

@GetMapping(value = "/{name}")
    @ApiOperation(value = "Find module",notes = "Find the module by Name")
    @ApiResponses(value = {
            @ApiResponse(code = 200,message = "Modules found"),
            @ApiResponse(code = 404,message = "Modules not found"),
    })

Api — название класса с описанием;
@ApiOperation название метода с описанием;
@ApiResponses возвращаемые коды API;
@ApiResponse конкретный код с описанием;


Пример (да в нем еще сущность main, которую в статье не описываю)


Теперь можно потестировать выборку данных по REST API.


Список используемой литературы:


1. https://www.baeldung.com
2. https://docs.spring.io
3. Spring in Action, 5th Edition
4. https://www.firebirdsql.org/file/documentation/drivers_documentation/java/faq.html#how-can-i-solve-the-error-connection-rejected-no-connection-character-set-specified

githublink


Гитхаб проекта

Комментарии (8)


  1. alek_sys
    06.12.2018 11:13
    +1

    В общем случае @Entity не должна быть @Data классом, т.к. дата-классы это Value-objects, и, например, проверяют идентичность объектов по всем полям. Для entity же идентичность обычно значит совпадение ID или NaturalId, это важно для многих внутренних механизмов JPA.


    Здесь Влад рассказывает как раз как правильно реализовать equals / hashCode.


    1. AlexisVaBel Автор
      06.12.2018 11:25

      спасибо за уточнение и ресурсы.


  1. xpendence
    06.12.2018 16:49

    Если Вы собираетесь в дальнейшем использовать этот код, Вам рано или поздно придётся распечатать Data и заменить на отдельные аннотации — Setter, ToString и проч. Иначе, у Вас будут существенные ограничения на кастомизацию полей.

    Логика в контроллерах? Ай-ай-ай. Хотя, о том, что CMainViewReport является контроллером, нужно ещё догадаться.

    Исключения ради исключений — тоже не очень хорошо.


    1. ialov
      07.12.2018 05:05

      Исключения ради исключений — тоже не очень хорошо.

      Сказал человек, генерирующий исключения на ровном месте и пишущий интерсепторы для их последующей обработки :)


    1. AlexisVaBel Автор
      07.12.2018 05:09

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


    1. AlexisVaBel Автор
      07.12.2018 05:10

      а по поводу Data alek_sys уже навел, что лучше бы его не юзать в entity. Спасибо, за доп. подтверждение.


  1. dev-priporov
    08.12.2018 11:57
    +1

    Здравствуйте, я посмотрел ваш код на гитхабе и написал несколько замечаний с рекомендациями, вообщем небольшое такое код-ревью провёл, раз уж вы выложили код на хабр:)

    1. для чего в CProductService аннотации @Transactional над самим классом и методами create и update? Они явно лишние на мой взгляд, что касается метода delete, я не помню его поведение в случае попытки удалить несуществующую сущность, по-моему падает эксепшн — что лучше, 2 раза сходить в БД или ловить эксепшны, вопрос спорный, если у кого есть какие мысли — пишите, мне тоже интересно
    2.Хьюстон, тут у нас явно проблемы…

    final CProduct [] arr=new CProduct[1];
            arr[0]=null;
            findAll().stream().forEach(x -> {if(name.compareTo(x.getName()) == 0){
                arr[0]=x;
            }});
    
            if(arr[0] != null){
                return arr[0];
            }else{
                throw new CProductNotFoundException(name);
            }

    a. Почему findAll()? вытаскивать все продукты из БД и бегать по ним стримом на стороне сервера в будущем будет очень жирно, тем более что уже есть метод productRepository.findByName
    b.
    name.compareTo(x.getName()) == 0
    — весьма странное решение, если вы не собираетесь ничего сортировать, достаточно equals или equalsIgnoreCase
    с. создавать массив с 1 элементом чтобы его поменять… примерно тоже самое будет выглядеть как-нибудь вот так:
    productRepository.findByName(name) 
                             .stream()
                             .filter(it -> it.getName().equalsIgnoreCase(name))
                             .findAny()
                             .orElseThrow(() -> new CProductNotFoundException(name))
    

    Но вообще я не понял смысла этого метода т.к. уже есть findByName, и по сигнатуре они похожи:
    public List<CProduct> findByName(String name)
    public CProduct findOne(String name) throws CProductNotFoundException 
    

    … можно было сделать что-то вроде:
            List<CProduct> products = productRepository.findByName(name);
            if (products.isEmpty()){
                throw new CProductNotFoundException(name);
            }
            return products.get(0);
    

    но ещё короче было бы сделать всё в репозитории с помощью аннотации Query(nativeQuery = true, ...)
    3. пункт 1 и 2 актуальны для всех сервисов как оказалось, т.к. там везде примерно одинаково
    4. вообще в моей практике пакет с @RestController'ами всегда так и называли — controller, реже — rest… но это уже на ваше предусмотрение, просто resources я ещё не встречал
    5. пакеты названы то в единственном числе, то во множественном — нужно единообразие
    6. возвращать из контроллеров ResponseEntity.ok нет смысла, они итак будут по-умолчанию 200, если нужны разные статусы, тогда ок, но можно было бы это делать через @ControllerAdvice, т.к. как правило логика примерно одинакова
    7. CHomeResource возвращает статику, можно было бы возвращать чистый html из ресурсов
    8. в CMViewRepResource
    @GetMapping
        public ResponseEntity<byte[]> report(String moduleName){
            byte[] bytes = null;
            try {
                bytes = jreportService.getMainReportPdfAsBytes(main_template_path);
                return ResponseEntity.ok()
                        .header("Content-Type", "application/pdf; charset=UTF-8")
                        .header("Content-Disposition", "inline; filename=\"" + moduleName + ".pdf\"")
                        .body(bytes);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

    очень странно что вы возвращаете null, когда у вас есть ResponseEntity, можно возвращать код с ошибкой
    9. совсем нет логирования
    10. JasperReportService не бин, он не будет инжектиться в CMViewRepResource, будет NPE
    11. лучше пользоваться интерфейсами, когда пишите сервисы


    1. AlexisVaBel Автор
      08.12.2018 11:59

      Не ожидал код ревью, огромная благодарность.