среда, 21 августа 2019 г.

Как добавить в Spring Boot приложение шрифты для библиотеки jasperreports. How to add fonts into Spring Boot application for the jasperreports library

Один из непростых квестов, с которыми мне пришлось столкнуться в последнее время - добавление китайских шрифтов для создания документов с помощью библиотеки jasperreports.
Решил так:

1. Скачал китайский true type шрифт ARIALUNI.TTF вот отсюда: https://community.jaspersoft.com/sites/default/files/files/Report24775-eastAsian.zip

2. Добавил в каталог resources проекта файл шрифтов, а также следующие файлы:
<?xml version="1.0" encoding="UTF-8"?>
<fontFamilies>
<fontFamily name="ARIALUNI">
<normal><![CDATA[fonts/ARIALUNI/ARIALUNI.TTF]]></normal>
<pdfEncoding><![CDATA[Identity-H]]></pdfEncoding>
<exportFonts/>
</fontFamily>
</fontFamilies>

net.sf.jasperreports.extension.registry.factory.fonts=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory
net.sf.jasperreports.extension.simple.font.families.ireportfamily1565796714618=fonts/fontsfamily1565796714618.xml

Место размещения файлов должно быть следующим:


3. Проверил доступность шрифтов для библиотеки jasperreports:
import net.sf.jasperreports.extensions.ExtensionsEnvironment;
import net.sf.jasperreports.engine.fonts.FontFamily;
public class Application {
public static void main(String args[]) {
List<FontFamily> families = ExtensionsEnvironment.getExtensionsRegistry().getExtensions(FontFamily.class);
System.out.println("Available fonts: " + families);
}
}

Как добавить шрифты в docker-контейнер. How to add fonts into docker container


Вариант 1: добавляем пакет MS Core Fonts:
# Base OS layer
FROM openjdk:8-jdk-alpine
# Install packages
RUN apk --no-cache add curl ttf-dejavu msttcorefonts-installer fontconfig \
&& update-ms-fonts \
&& fc-cache -f
view raw Dockerfile hosted with ❤ by GitHub
Вариант 2: добавляем отдельно ttf-файл шрифтов:
FROM openjdk:8-jdk-alpine
# Install font file
RUN mkdir -p /usr/share/fonts/truetype/myfont
COPY MYFONT.TTF /usr/share/fonts/truetype/myfont
RUN fc-cache -fv
view raw Dockerfile hosted with ❤ by GitHub
Проверяем доступность шрифтов в системе:
import java.awt.GraphicsEnvironment;
public class Application {
public static void main(String args[]) {
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
System.out.println("Avialable fonts:");
for (String fontName : env.getAvailableFontFamilyNames()) {
System.out.println(fontName);
}
}
}

четверг, 15 августа 2019 г.

Как в Hibernate создать отображение многие-ко-многим через внешнюю таблицу, имеющую составной первичный ключ

Hibernate: How to map ManyToMany association with external table and composite key?
Этот интересный вопрос возник передо мной, когда на новом проекте разработка началась по принципу DDD - т.е. сначала была создана база данных со всеми таблицами и сущностями, а потом всё это нужно было реализовать в java-коде.
Честно скажу, повозиться пришлось с этим кодом немало. Зато теперь всё работает как часы и опыта в копилку прибавилось.
Итак, имеем следующую базу данных:

CREATE TABLE IF NOT EXISTS post
(
id uuid PRIMARY KEY,
title varchar(255)
);
CREATE TABLE IF NOT EXISTS tag
(
id uuid PRIMARY KEY,
name varchar(255)
);
CREATE TABLE IF NOT EXISTS post_tag
(
post_id uuid NOT NULL,
tag_id uuid NOT NULL,
PRIMARY KEY (post_id, tag_id)
);
view raw script.sql hosted with ❤ by GitHub
Не мудрствуя лукаво, создал базу такую же, как здесь.
Связь многие-ко-многим между двумя сущностями через промежуточную таблицу может быть представлена в виде двух связей многие к одному с промежуточным классом.
Создаём классы-сущности:
@Data
@Entity
@Table(name = "post")
@EqualsAndHashCode
public class Post {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "uuid2")
private UUID id;
@NotNull
@Column(name = "title")
private String title;
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval=true)
private Set<PostTag> postTags = new HashSet<>();
}
view raw Post.java hosted with ❤ by GitHub
@Data
@Entity
@Table(name = "tag")
@EqualsAndHashCode
public class Tag {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "uuid2")
private UUID id;
@NotNull
@Column(name = "name")
private String name;
@OneToMany(mappedBy = "tag", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval=true)
private Set<PostTag> postTags = new HashSet<>();
}
view raw Tag.java hosted with ❤ by GitHub

Параметр orphanRemoval=true сообщает фреймворку Hibernate, что тот должен навсегда удалять объекты при удалении их из коллекции.
Параметр fetch = FetchType.LAZY говорит о том, что коллекция будет загружаться только при прямом обращении к ней (по требованию) - т.н. стратегия отложенной загрузки. Для реализации такой отложенной загрузки Hibernate использует вместо объектов-коллекций сгенерированные во время выполнения заглушки, называемые прокси-объектами.
Создаём класс, который будет отображать связь многие-ко-многим между классами Tag и Post:
@Immutable
@Entity
@Table(name = "post_tag")
@Getter
@EqualsAndHashCode
@NoArgsConstructor
public class PostTag {
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@Embeddable
public static class Id implements Serializable {
private static final long serialVersionUID = 1L;
@Column(name = "post_id")
protected UUID postId;
@Column(name = "tag_id")
protected UUID tagId;
}
@EmbeddedId
protected PostTag.Id id = new PostTag.Id();
@ManyToOne
@JoinColumn(name = "post_id", insertable = false, updatable = false)
private Post post;
@ManyToOne
@JoinColumn(name = "tag_id", insertable = false, updatable = false)
private Tag tag;
public PostTag(Post post, Tag tag) {
this.post = post;
this.tag = tag;
this.id.postId = post.getId();
this.id.tagId = tag.getId();
post.getPostTags().add(this);
tag.getPostTags().add(this);
}
view raw PostTag.java hosted with ❤ by GitHub
Что в этой промежуточной сущности интересно:
- аннотация @Immutable объявляет класс неизменяемым. Это позволяет Hibernate не проверять состояние объекта во время выталкивания контекста;
- встроенный класс PostTag.Id инкапсулирует составной ключ, состоящий из полей post_id и tag_id;
- @Embedable объявляет класс PostTag.Id встроенным в таблицу класса PostTag;
- @EmbeddedId - делает почти то же самое, что @Embedable, только для столбцов первичного ключа;
- конструктор класса PostTag, в котором производится формирование первичного ключа и двунаправленной связи многие-ко-многим между объектами Post и Tag. Эту связь очень просто сделать однонаправленной. Для этого в сущности, в которой отображение этой связи не нужно, необходимо убрать поле Set<PostTag> postTags и из конструктора класса PostTag убрать добавление созданного объекта PostTag в это поле.

Как упрощённо выглядит в java-коде создание и удаление связи между объектами Post и Tag на примере класса PostService:
@Service
public class PostService {
@Autowared
private PostRepository postRepository;
@Autowared
private TagRepository tagRepository;
@Transactional
public void addTagToPost(UUID postId, UUID tagId) {
postRepository.findById(postId).ifPresent(post -> {
tagRepository.findById(tagId).ifPresent(tag -> new PostTag(post, tag));
});
}
@Transactional
public void deleteTagFromThePost(UUID postId, UUID tagId) {
postRepository.findById(postId).ifPresent(post -> {
post.getPostTag().removeIf(pt -> tagId.equals(pt.getTag().getId()));
});
}
}
Всё доволно просто:
- для создания связи нужно создать объект PostTag,
- для разрушения связи - удалить соответствующий объект PostTag из коллекции объекта Post.
Всё остальное за нас делает Hibernate.

П.с.: В классах используются аннотации фреймворков Lombok, Spring и Hibernate.

четверг, 1 августа 2019 г.

Учёба

Пару недель назад начал проходить курс React + Redux - Профессиональная Разработка .
Прошёл уже 120 уроков из 149, выполнил 3 учебных проекта. Осталось немного и можно будет пилить какое-нибудь своё приложение на React, чтоб не потерять приобретённые навыки.
Прошёл курс по тестированию web UI: End to End Testing with Google's Puppeteer and Jest
Учебные проекты можно посмотреть здесь, здесь и здесь.
Скриншоты того, что получилось:





Строим очередь сообщений на Apache Kafka с помощью Spring Cloud Stream

Почему Spring Cloud Stream?
Потому что в этом случае для замены в дальнейшем Apache Kafka на другой менеджер очереди (например, RabbitMQ) понадобится всего лишь:
- добавить соответствующие зависимости в файл pom.xml;
- скорректировать настройки в файле application.properties для новой системы сообщений.

Итак, приступим:

1. Добавим в pom.xml зависимости для Apache Kafka.

<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-stream-binder-kafka-streams</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>

Далее мы будем просто переключать профили Spring для использования RabbitMQ или Apache Kafka.

2. Добавим в файл application.properties настройки очереди Apache Kafka и свои заголовки.

# Apache Kafka properties
# general
spring.cloud.stream.kafka.binder.brokers=localhost:9092
spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000
spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde=org.apache.kafka.common.serialization.Serdes$StringSerde
spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde=org.apache.kafka.common.serialization.Serdes$StringSerde
#
# outputChannel (out)
spring.cloud.stream.bindings.paymentEventsChannel.producer.headerMode=headers
spring.cloud.stream.bindings.paymentEventsChannel.destination=output
#
# custom headers
spring.cloud.stream.kafka.binder.headers=SenderId,SenderName,Type,MessageId
#
# inputChannel (in)
spring.cloud.stream.bindings.inputChannel.consumer.headerMode=headers
spring.cloud.stream.bindings.inputChannel.destination=input
spring.cloud.stream.bindings.inputChannel.group=queue

3. Создадим свой обработчик каналов сообщений.
Можно использовать имеющиеся в Spring Cloud Stream интерфейсы Source (output channel), Sink (input channel) или Processor (input & output channels), но полезнее будет сделать всё "ручками". Тем более, что в свой обработчик каналов мы можем добавить любое количество каналов.

@Slf4j
@Component
@EnableBinding(ChannelProcessor.class)
public class MessageListener {

    @StreamListener(ChannelProcessor.INPUT)
    public void listen(String message) {
        log.info("MessageListener got message: {}.", message);
    }
}
5. Создадим производителя сообщений, который будет передавать в качестве сообщения наш объект Request.

@Component
public class MessagePublisher {

    private ChannelProcessor channelProcessor;

    @Autowired
    public MessagePublisher(ChannelProcessor channelProcessor) {
        this.channelProcessor = channelProcessor;
    }

    public void sendRequest(Request request) {
        channelProcessor.outputChannel().send(message(request));
    }

    private static final <T> Message<T> message(T val) {
        return MessageBuilder.withPayload(val).build();
    }
}

6. Создадим слушателя очереди, который будет логировать сообщения, полученные из очереди.

@Slf4j
@Component
@EnableBinding(ChannelProcessor.class)
public class MessageListener {

    @StreamListener(ChannelProcessor.INPUT)
    public void listen(Message<QueueMessage> msg) {

        QueueMessage message = msg.getPayload();
        MessageHeaders headers = msg.getHeaders();
        String messageType = (String) headers.get(MessageDefinitions.TYPE);
        final String token = extractToken(headers);
        final String clientIP = extractClientIP(headers);
        log.info("Got message: {}, client IP: {}, token: {}, message type: {}.",
                message.toString(), clientIP, token, messageType);
    }

    private String extractToken(MessageHeaders headers) {
        return (String) headers.get(MessageDefinitions.SENDER_ID);
    }

    private String extractClientIP(MessageHeaders headers) {
        return (String) headers.get(MessageDefinitions.SENDER_NAME);
    }
}

Аннотация @EnableBinding указывает на то, с каким обработчиком каналов будет связан наш слушатель. @StreamListener - сообщения из какого канала обрабатывает этот метод.

На этом всё.