Показаны сообщения с ярлыком Spring. Показать все сообщения
Показаны сообщения с ярлыком Spring. Показать все сообщения

суббота, 24 февраля 2018 г.

Взаимодействие SpringBoot с RabbitMq

RabbitMQ - одно из популярных решений для отправки сообщений, предоставляется клиентские библиотеки для таких языков как Java, Scala, .NET, Go, Python, PHP  и другие. В данном турториале мы рассмотрим отправку и обработку сообщений, а так же как отправлять сообщение с JSON содержимым и работать с ошибками при помощи Dead Letter Queues (DLQ)

Для начала установим сервер RabbitMQ на вашей локальной машине, как указано здесь или запустим с помощью docker-compose:

version: '3'
services:
  rabbitmq:
    container_name: rabbitmq
    image: 'rabbitmq:management'
    ports:
      - "5672:5672"
      - "15672:15672"
 
Теперь можно запустить RabbitMQ используя docker-compose up и открыть UI администратора по адресу http://localhost:15672/.

Если вы знакомы с другими брокерскими службами обмена сообщениями, такими как ActiveMQ,  в них обычно используются очереди(Queues) и топики (Topics) для отправки индивидуальной и общедоступной модели связи. В RabbitMQ используется Exchange и в зависимости от ключа маршрутизации, сообщение будет отправлено в очередь (очереди) (Queues). Вы можете больше узнать о концепциях RabbitMQ здесь.

Приложение SpringBoot c RabbitMQ

Создадим SpringBoot приложение, описав следующие стартеры:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.sivalabs</groupId>
    <artifactId>springboot-rabbitmq-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RC1</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
      
</project>
Начнем с конфигурации RabbitMQ, создадим RabbitConfig и опишем следующие бины: Queue, Exchange и Binding:
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
  
@Configuration
public class RabbitConfig 
{
    public static final String QUEUE_ORDERS = "orders-queue";
    public static final String EXCHANGE_ORDERS = "orders-exchange";

    @Bean
    Queue ordersQueue() {
        return QueueBuilder.durable(QUEUE_ORDERS).build();
    }

    @Bean
    Queue deadLetterQueue() {
        return QueueBuilder.durable(QUEUE_DEAD_ORDERS).build();
    }
    @Bean
    Exchange ordersExchange() {
        return ExchangeBuilder.topicExchange(EXCHANGE_ORDERS).build();
    }

    @Bean
    Binding binding(Queue ordersQueue, TopicExchange ordersExchange) {
        return BindingBuilder.bind(ordersQueue).to(ordersExchange).with(QUEUE_ORDERS);
    }
}
Мы объявляем Queue имен orders-queue и Exchange с именем orders-exchange.
Также определили привязку между orders-queue и orders-exchange, чтобы любое сообщение, отправленное на orders-exchange с ключом направления как orders-queue, отправлялось в orders-queue.

Конфигурация RabbitMQ представлена в application.properties:

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
Создадим бин OrderMessageSender для отправки сообщений в orders-exchange. Spring Boot автоматически конфигурирует инфраструктурные бины, необходимые для отправки/обработки сообщений в/от брокера сообщений RabbitMQ. Нам необходимо просто проинжектить RabbitTemplate и отправить сообщение вызвав метод  rabbitTemplate.convertAndSend(“routingKey”, Object).

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Order implements Serializable{
    private String orderNumber;
    private String productId;
    private double amount;
}

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderMessageSender {
    private final RabbitTemplate rabbitTemplate;
    @Autowired
    public OrderMessageSender(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    public void sendOrder(Order order) {
        this.rabbitTemplate.convertAndSend(RabbitConfig.QUEUE_ORDERS, order);

    }
}
По умолчанию в SpringBoot используется org.springframework.amqp.support.converter. SimpleMessageConverterand, который сериализует объект в byte[].
После отправки сообщения вы можете просмотреть сообщение из интерфейса администратора, выполнив вход с учетными данными guest/guest.

Теперь создадим слушателя, используя аннотацию RabbitListener.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import ru.config.Config;
import ru.model.Order;

@Component
public class OrderMessageListener {
    static final Logger logger = LoggerFactory.getLogger(OrderMessageListener.class);

    @RabbitListener(queues = Config.QUEUE_ORDERS)
    public void process(Order order){
        logger.info("Received order");
        logger.info(order.toString());
    }

}

Вот и все! Просто добавив @RabbitListener и определив, какую очередь слушать, мы можем создать слушателя.

Теперь, если вы отправляете сообщение в очередь, то с помощью метода OrderMessageListener.process(), вы обработаете данное сообщение и в логах приложения будет написано Received order и само содержимое объекта.

Отправка и прием сообщений c JSON содержимым.

Как мы видели, механизм сериализации по умолчанию преобразует объект сообщения в byte [] с помощью SimpleMessageConverter и на принимающей стороне, он будет десериализован из byte [] в тип объекта (в нашем случае Order) с помощью GenericMessageConverter.

Чтобы изменить это поведение, нам нужно настроить автоконфигурацию бинов Spring Boot RabbitMQ.

Отправить сообщение как JSON

Одним быстрым способом отправки сообщения с JSON является использование ObjectMapper, мы можем конвертировать Orderobject в JSON и отправлять его.
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.config.Config;
import ru.model.Order;

@Service
public class OrderMessageService {
    private final RabbitTemplate rabbitTemplate;
    private ObjectMapper objectMapper;

    @Autowired
    public OrderMessageService(RabbitTemplate rabbitTemplate, ObjectMapper objectMapper) {
        this.rabbitTemplate = rabbitTemplate;
        this.objectMapper = objectMapper;
    }

    public void sendOrder(Order order) throws Exception{
        String orderJson = objectMapper.writeValueAsString(order);
        Message message = MessageBuilder
                .withBody(orderJson.getBytes())
                .setContentType(MessageProperties.CONTENT_TYPE_JSON)
                .build();

        rabbitTemplate.convertAndSend(Config.QUEUE_ORDERS, order);
    }
}
Но преобразование объектов в JSON, как это, является своего рода избыточностью. Вместо этого мы можем сделать изменить конфигурацию, настроив компонент org.springframework.amqp.support.converter.Jackson2JsonMessageConverter, который будет использоваться в RabbitTemplate, чтобы сообщение было сериализовано как JSON вместо byte[].
@Configuration
public class RabbitConfig{
    ...
    ...

    @Bean
    public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
        final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(producerJackson2MessageConverter());
        return rabbitTemplate;
    }

    @Bean
    public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}
Теперь сообщения отправляются в виде JSON. Так же необходимо настроить прием JSON сообщения. Для этого необходимо имплиментировать для нашего класса конфигурций интерфейс RabbitListenerConfigurer.
 @Configuration
public class RabbitConfig implements RabbitListenerConfigurer {
    ...
    ...
    @Override
    public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
        registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory());
    }
    @Bean
    MessageHandlerMethodFactory messageHandlerMethodFactory() {
        DefaultMessageHandlerMethodFactory messageHandlerMethodFactory = new DefaultMessageHandlerMethodFactory();
        messageHandlerMethodFactory.setMessageConverter(consumerJackson2MessageConverter());
        return messageHandlerMethodFactory;
    }
  
    @Bean
    public MappingJackson2MessageConverter consumerJackson2MessageConverter() {
        return new MappingJackson2MessageConverter();
    }
}

Обработка ошибок и невалидных сообщений, используя DeadLetterQueues(DLQ)

Мы можем отправить невалидные сообщения в отдельную очередь, чтобы мы могли их проверять и обрабатывать позже. Для этого можно использовать концепцию DLQ для автоматизации, вместо написания кода и обработки такого события.
Мы можем объявить dead-letter-exchange, dead-letter-routing-key для очереди, тогда объявление бина будет следующим:

@Configuration
public class RabbitConfig implements RabbitListenerConfigurer {

    public static final String QUEUE_ORDERS = "orders-queue";
    public static final String EXCHANGE_ORDERS = "orders-exchange";
    public static final String QUEUE_DEAD_ORDERS = "dead-orders-queue";

    @Bean
    Queue ordersQueue() {
        return QueueBuilder.durable(QUEUE_ORDERS)
                .withArgument("x-dead-letter-exchange", "")
                .withArgument("x-dead-letter-routing-key", QUEUE_DEAD_ORDERS)
                .withArgument("x-message-ttl", 15000) //Если сообщение не бработано за 15 сек - оно отправляется в DLQ
                .build();

    }
 
    @Bean
    Queue deadLetterQueue() {
        return QueueBuilder.durable(QUEUE_DEAD_ORDERS).build();
    }

    ...
    ...
}

Теперь можно отправить сообщение с JSON отличным от объекта Order и оно попадет в очередь dead-orders-queue. Необходимо заметить, что если очередь orders-queue была создана - ее необходимо удалить, чтобы приложение запустилось.

PS это мой перевод данной статьи

суббота, 1 июля 2017 г.

Spring boot. Spring Security

Рассмотрим возможности Spring Security. В качестве примера, воспользуемся веб-проектом из предыдущих статей. Первым делом, необходимо добавить зависимость Spring Security в наш pom-файл:
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
Собственно, этого остаточно, для самого простого примера. Если запустить наш пример и посмотреть на консоль вывода, можно заметить следующее сообщение:

Данный пароль можно использовать, если обратиться к localhost:8080:
Попробуем немного усложнить пример и создадим application.properties файл с таким содержимым:
security.user.name = user
security.user.password = password
Теперь, запустив наш проект, мы не увидим в консоли вывод пароль, в данном случае нам подойдут данные, которые мы указали в проперти файле.

Далее, рассмотрим In-memory security. Для этого необходимо создать следующий класс:
package com.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableGlobalAuthentication
public class InMemorySecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").
                roles("USER")
                .and().withUser("admin").password("password").
                roles("USER", "ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/journal/**").authenticated()
                .and()
                .httpBasic();
    }
}

В данном классе:

  • Aннотация EnableGlobalAuthentication - говорит о том, что класс с данной аннотацией используется для конфигурирования AuthenticationManagerBuilder.
  • AuthenticationManagerBuilder инжектится благодаря аннотации Autowired
  • WebSecurityConfigurationAdapter - абстрактный класс, предоставляющий удобную базу для конфигурации инстанса WebSecurityConfigurer
  • configure - переопределенный метод, для настройки HttpSecurity. В данном случае, достп по адресу "/" предоставляется свободный, а для /journal/** - необходима авторизация



суббота, 24 июня 2017 г.

Spring boot. JDBC

Рассмотрим, как с помощью Spring Boot, можно обращаться к БД, используя шаблоны JDBC. Рассмотрим pom файл:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>spring-boot-jdbc-template</groupId>
    <artifactId>jdbc-template</artifactId>
    <version>1.0</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
Сущность для манипуляции:

package com.entity;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Journal {
    private Long id;
    private String title;
    private Date created;
    private String summary;
    private SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy");
    public Journal(Long id, String title, String summary, Date date){
        this.id = id;
        this.title = title;
        this.summary = summary;
        this.created = date;
    }
    Journal(){}

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public Date getCreated() {
        return created;
    }
    public void setCreated(Date created) {
        this.created = created;
    }
    public String getSummary() {
        return summary;
    }
    public void setSummary(String summary) {
        this.summary = summary;
    }
    public String getCreatedAsShort(){
        return format.format(created);
    }
    public String toString(){
        StringBuilder value = new StringBuilder("* JournalEntry(");
        value.append("Id: ");
        value.append(id);
        value.append(",Title: ");
        value.append(title);
        value.append(",Summary: ");
        value.append(summary);
        value.append(",Created: ");
        value.append(getCreatedAsShort());
        value.append(")");
        return value.toString();
    }
}

Далее создадим сервис. У него будет две функции: добавление новой записи в БД и получение всех записей.

package com.service;

import com.entity.Journal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Service
public class JournalService {
    private static final Logger log = LoggerFactory.getLogger(JournalService.class);

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void insertData(){
        log.info("> Table creation");
        jdbcTemplate.execute("DROP TABLE JOURNAL IF EXISTS");
        jdbcTemplate.execute("CREATE TABLE JOURNAL(id SERIAL, title VARCHAR(255), summary VARCHAR(255), created TIMESTAMP)");
        log.info("> Inserting data...");
        jdbcTemplate.execute("INSERT INTO JOURNAL(title,summary,created) VALUES('Get to know Spring Boot','Today I will learn Spring Boot'," +
                        "'2016-01-01 00:00:00.00')");
        jdbcTemplate.execute("INSERT INTO JOURNAL(title,summary,created) VALUES('Simple Spring Boot Project','I will do my first Spring Boot" +
                                "project','2016-01-02 00:00:00.00')");
        jdbcTemplate.execute("INSERT INTO JOURNAL(title,summary,created) VALUES('Spring Boot Reading','Read more about Spring Boot'," +
                        "'2016-02-01 00:00:00.00')");
        jdbcTemplate.execute("INSERT INTO JOURNAL(title,summary,created) VALUES('Spring Boot in the Cloud','Learn Spring Boot using Cloud" +
                                "Foundry','2016-01-01 00:00:00.00')");
        log.info("> Done.");
    }

    public List<Journal> findAll(){
        List<Journal> entries = new ArrayList<>();
        jdbcTemplate.query("SELECT * FROM JOURNAL",
                new Object[]{},
                (rs,row) -> new Journal(rs.getLong("id"),
                        rs.getString("title"), rs.getString("summary"),
                        new Date(rs.getTimestamp("created").getTime())))
                        .forEach(entries::add);
        return entries;
    }

}
  • JdbcTemplate - автосвязанный класс, который отвечает за выполнение запросов для БД.
  • insertDate - первым делом, метод пытается удалить таблицу JOURNAL, если она есть, далее создает таблицу занова и добавляет в нее записи
  • findAll - получает,с помощью RowMapper, все записи из таблицы JOURNAL.
И класс запуска приложения:
package com;

import com.service.JournalService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {
    private static final Logger log = LoggerFactory.getLogger(Application.class);
    @Autowired
    private JournalService service;

    public static void main(String[] arg){
        SpringApplication.run(Application.class, arg);
    }

    @Override
    public void run(String... strings) throws Exception {
        log.info("@@ Inserting Data....");
        service.insertData();
        log.info("@@ findAll() call...");
        service.findAll().forEach(entry -> log.info(entry.toString()));
    }
}

Данный класс имплементирует интерфейс CommandLineRunner, для этого необходимо переопределить метод run, он выполнится после старта приложения.

воскресенье, 11 июня 2017 г.

Spring boot. Конфигурация XML

Предположим, у нас есть приложение написанное с помощью Spring Boot, а так же есть конфигурация на xml, рассмотрим, как использовать их в одном приложении:
Есть 2 сущности, кошки и пользователи:

package com.entity;

public class User {
    private String name;
    private String surname;

    public User() {
    }

    public User(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }
}
package com.entity;

public class Cat {
    private String name;
    private int age;

    public Cat(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Контроллер для пользователей и для котов, обратите внимание, они находятся в разных пакета:
package com.controller;

import com.entity.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
public class UserController {

    private static List users = new ArrayList(){{
       add(new User("name", "surname"));
       add(new User("name2", "surname2"));
       add(new User("name3", "surname3"));
       add(new User("name4", "surname4"));
       add(new User("name5", "surname5"));
    }};

    @RequestMapping("/api/users/all")
    public List getAllUser(){
        return users;
    }
}

package ru.controller.web;

import com.entity.Cat;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
public class CatController {
    private static List cats = new ArrayList(){{
        add(new Cat("name",1));
        add(new Cat("name2",2));
        add(new Cat("name3",3));
        add(new Cat("name4",4));
    }};

    @RequestMapping("/api/cats/all")
    public List getCats(){
        return cats;
    }
}

Конфиг xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       ">
    <context:component-scan base-package="ru.controller.*"/>


</beans>
И инициализатор для Spring Boot:
package com;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

@SpringBootApplication(scanBasePackages = "com.*")
@ImportResource("classpath*:context.xml")
public class Application {
    public static void main(String[] arg){
        SpringApplication.run(Application.class, arg);
    }
}

Необходимо обратить внимание, на то, что сканирование просиходит из пакета com.*, т.е. контроллер для кошек - игнорирутся. Подключение xml конфигурации происходит с помощью аннотации ImportResources, в него передается массив с путями до xml конфигураций. Запустив приложение, видим, что оба пути /api/cats/all и /api/users/all - работают.

воскресенье, 28 мая 2017 г.

Spring boot. Часть 2 Настройки

Рассмотрим некоторые возможности конфигурации приложения.

Исключение определенных конфигураций.

Чтобы не использовать какую то автоконфигурацию достаточно просто исключить ее в аннотации SpringBootApplication:

@SpringBootApplication( exclude={ActiveMQAutoConfiguration.class} )
И данный класс не учавствует в настройках, достаточно удобно.

Настройка для SpringApplication

Для того, чтобы иметь доступ к дополнительным конфигурациям необходимо создать инстанс класса:

SpringApplication app = new SpringApplication(Application.class);
        app.run(arg);
Например, можно заменить баннер спринга, который печатется при запуске приложения:

SpringApplication app = new SpringApplication(Application.class);
        app.setBanner((environment, aClass, printStream) -> printStream.println("\n\n\nThis is my banner!\n\n"));
        app.run(arg);
Или вообще его отключить:
SpringApplication app = new SpringApplication(SpringBootSimpleApplication.class);
app.setBannerMode(Mode.OFF);
app.run(args);

Так же, используя билдер, можно добавить профили или слушателя на события приложения:

new SpringApplicationBuilder(Application.class)
                .listeners((ApplicationListener<ApplicationEvent>) event ->
                        log.info("#### > " + event.getClass().getCanonicalName())).profiles("prod","cloud")
                .banner((environment, aClass, printStream) -> {
                    printStream.println("\n\n\nMy new Banner!\n\n");
                })
                .run(arg);

Настройка через properties

Создадим файл application.properties, в нем добавим строку:
data.server=remoteserver:3030
К ней можно легко обратиться, рассмотрим пример из прошлого проекта. Контроллер  JournalController:
 @Value("${data.server}")
    private String server;
@RequestMapping("/server")
    public @ResponseBody String getServer(){
        return server;
    }


суббота, 27 мая 2017 г.

Spring boot Часть 1

Создадим первое приложение на Spring boot, пусть оно будеть чуть сложнее, чем HelloWorld! Начнем рассмотрение с pom файла:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
          http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>spring-boot-journal</groupId>
    <artifactId>spring-boot-journal</artifactId>
    <version>1.0</version>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
Далее создадим сущность:

package ru.domain;

import javax.persistence.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

@Entity
public class Journal {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String title;
    private Date create;
    private String summary;
    @Transient
    private SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy");

    public Journal(){
    }

    public Journal(String title, String summary,String create) throws ParseException{
        this.title = title;
        this.create = format.parse(create);
        this.summary = summary;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Date getCreate() {
        return create;
    }

    public void setCreate(Date create) {
        this.create = create;
    }

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }

    public String getCreatedAsShort(){
        return format.format(create);
    }

    public String toString(){
        return "JournalEntry(" + "Id: " +
                id +
                ",Title: " +
                title +
                ",Summary: " +
                summary +
                ",Created: " +
                getCreatedAsShort() +
                ")";
    }
}

Здесь ничего необычного, т.к. используем JPA - значит нам нужный аннотации Entity, ID и GeneratedValue. Используем 2 коструктора, с параметрами для нашего удобства и без парамтеров - для JPA. Аннотация Transient, говорит о том, что это поле не будет мапиться на таблицу.
Далее создадим репозиторий, используя SpringData

package ru.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import ru.domain.Journal;

public interface JournalRepository extends JpaRepository<Journal, Long> {
}

Данный интерфейс расширяет JpaRepository, что позволяет спрингу, с помощью proxy создавать не только просты CRUD действия, но и более сложные используя конвенцию имен, например, findById, findByTitle, findByTitleLike, findByTitleLikeIgnoreCase.
Теперь веб контроллер:

package ru.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import ru.domain.Journal;
import ru.repository.JournalRepository;

import javax.annotation.Resource;
import java.util.List;

@Controller
public class JournalController {
    @Resource
    private JournalRepository repository;

    @RequestMapping("/journal")
    public @ResponseBody List<Journal>getJournals(){
        return repository.findAll();
    }

    @RequestMapping("/")
    public String index(Model model){
        model.addAttribute("journal", repository.findAll());
        return "index";
    }
}
Ну и сама веб страничка:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" >
<head>
    <meta charset="UTF-8"/>
    <meta http-equiv="Content-Type" content="text/html"/>
    <title>Spring Boot Journal</title>
    <link rel="stylesheet" type="text/css" media="all" href="css/bootstrap.min.css"/>
    <link rel="stylesheet" type="text/css" media="all" href="css/bootstrap-glyphicons.css"/>
    <link rel="stylesheet" type="text/css" media="all" href="css/styles.css"/>
</head>
<body>
<div class="container">
    <h1>Spring Boot Journal</h1>
    <ul class="timeline">
        <div  th:each="entry,status : ${journal}" >
            <li  th:attr="class=${status.odd}?'timeline-inverted':''" >
                <div class="tl-circ"></div>
                <div class="timeline-panel">
                    <div>
                        <h4> <span th:text="${entry.title}">TITLE</span>
                        </h4>
                        <p><small class="text-muted">
                            <i class="glyphicon glyphicon-time"></i>
                            <span th:text="${entry.createdAsShort}">CREATED</span>
                        </small>
                        </p>
                    </div>

                    <div class="tl-body">
                        <p> <span th:text="${entry.summary}">SUMMARY</span> </p>
                    </div>
                </div>
            </li>
        </div>
    </ul>
</div>
</body>
</html>
Ну и сам запуск приложения:

package ru;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration;
import org.springframework.context.annotation.Bean;
import ru.domain.Journal;
import ru.repository.JournalRepository;

@SpringBootApplication( exclude={ActiveMQAutoConfiguration.class} )
public class Application {
    public static void main(String[] arg){
        SpringApplication.run(Application.class, arg);
    }

    @Bean
    InitializingBean saveData(JournalRepository repository){
        return () -> {
            repository.save(new Journal("Get to know Spring Boot","Today I will learn Spring Boot","01/01/2016"));
            repository.save(new Journal("Simple Spring Boot Project","I will do my first Spring Boot Project","01/02/2016"));
            repository.save(new Journal("Spring Boot Reading","Read more about Spring Boot","02/01/2016"));
            repository.save(new Journal("Spring Boot in the Cloud","Spring Boot using Cloud Foundry","03/01/2016"));
        };
    }
}

Запустив приложение, увидим:
Отлично, все работает! Но за счет чего? Ведь нет ни web.xml, ни xml или java конфигов для сприга. Если открыть аннотацию SpringBootApplication, то мы увидим следующее:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    @AliasFor(
        annotation = EnableAutoConfiguration.class,
        attribute = "exclude"
    )
    Class<?>[] exclude() default {};

    @AliasFor(
        annotation = EnableAutoConfiguration.class,
        attribute = "excludeName"
    )
    String[] excludeName() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackages"
    )
    String[] scanBasePackages() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackageClasses"
    )
    Class<?>[] scanBasePackageClasses() default {};
}

И после этого становится все понятнее. Основная аннтоция - EnableAutoConfiguration. С ее помощью спринг использует автоконфигуратор, опираясь на classpath, аннотация, которые используются в коде. Например, спринг заметит, что у нас используется зависимость spring-boot-starter-web, и поймет, что у нас веб приложение и будет конфигурировать наше приложение, как веб. Так же к приложению будет вшит Tomcat для запуска и после сборки проекта, через mvn package мы сможем запустить наше приложение не используя дополнительный сервер приложений.

воскресенье, 12 марта 2017 г.

Sprig boot, Spring Data интеграция с JPA

Сейчас, интеграция Spring и JPA стало прстым и понятным благодаря Spring boot и Spring Data. Рассмотрим его. Так же используем Docker. Начнем с него.
Настроим PostgresSql:

docker pull postgres
#run the container
docker run --name postgreslocal -e POSTGRES_PASSWORD=postgres -d postgres
#get the ip
docker inspect --format '{{ .NetworkSettings.IPAddress }}' postgreslocal
#get the port
docker inspect --format '{{ .NetworkSettings.Ports }}' postgreslocal


Создадим таблицу работников (Employees)

create schema spring_data_jpa_example;

create table spring_data_jpa_example.employee(
    id  SERIAL PRIMARY KEY,
    firstname   TEXT    NOT NULL,
    lastname    TEXT    NOT NULL,  
    email       TEXT    not null,
    age         INT     NOT NULL,
    salary         real,
    unique(email)
);
 
insert into spring_data_jpa_example.employee (firstname,lastname,email,age,salary)
values ('Emmanouil','Gkatziouras','gkatzioura@gmail.com',18,3000.23);


Далее gradle-файл
group 'com.gkatzioura'
version '1.0-SNAPSHOT'

apply plugin: 'java'
 
sourceCompatibility = 1.8

buildscript {

    repositories {
       mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE")
    }
}
 
apply plugin: 'idea'
apply plugin: 'spring-boot'
 
repositories {
    mavenCentral()
}
 
dependencies {
    compile("org.springframework.boot:spring-boot-starter-web") {
        exclude module: "spring-boot-starter-tomcat"
    }
    compile("org.postgresql:postgresql:9.4-1206-jdbc42")
    compile("org.springframework.boot:spring-boot-starter-jetty")
    compile("org.springframework.boot:spring-boot-starter-data-jpa:1.3.3.RELEASE")
    compile("com.mchange:c3p0:0.9.5.2")
    testCompile("junit:junit:4.11");
}

Обратите внимание, мы добавили пул соединений с3p0, spring-boot-starter-data-jpa для Hibernate  и драйвер для Postgress. Это все, что нам нужно.

Класс Application:
package com.gkatzioura.springdata.jpa;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
 
/**
 * Created by gkatzioura on 6/2/16.
 */
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
 
        SpringApplication springApplication = new SpringApplication();
        ApplicationContext ctx = springApplication.run(Application.class, args);
    }

}

Конфигурация Datasource:
package com.gkatzioura.springdata.jpa.config;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import javax.sql.DataSource;
/**
 * Created by gkatzioura on 6/2/16.
 */
@Configuration
public class DataSourceConfig {
 
    @Bean
    public DataSource createDataSource() throws Exception {
 
        ComboPooledDataSource ds = new ComboPooledDataSource();
        ds.setJdbcUrl("jdbc:postgresql://172.17.0.3:5432/postgres?user=postgres&password=postgres");
        ds.setDriverClass("org.postgresql.Driver");
 
        return ds;
    }
 
}

Конфигурация JPA:
package com.gkatzioura.springdata.jpa.config;
 
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
import javax.sql.DataSource;
import java.util.Properties;
 
/**
 * Created by gkatzioura on 6/2/16.
 */

@Configuration
@EnableJpaRepositories(entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "transactionManager",
        basePackages = {"com.gkatzioura.springdata.jpa.persistence"})
@EnableTransactionManagement
public class JPAConfig {
 
    @Autowired
    private DataSource dataSource;
 
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
 
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setDataSource(dataSource);
        entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistenceProvider.class);
        entityManagerFactoryBean.setJpaProperties(hibernateProperties());
        entityManagerFactoryBean.setPackagesToScan(new String[] {"com.gkatzioura.springdata.jpa.persistence"});
        return entityManagerFactoryBean;
    }
 
    @Bean
    public JpaTransactionManager transactionManager() {
 
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return transactionManager;
    }
 
    private Properties hibernateProperties() {
 
        Properties properties = new Properties();
        properties.put("hibernate.dialect","org.hibernate.dialect.PostgreSQL9Dialect");
        properties.put("hibernate.globally_quoted_identifiers","true");
 
       return properties;
    }
}


Наша сущность для таблицы работников
package com.gkatzioura.springdata.jpa.persistence.entity;
 
import javax.persistence.*;

/**
 * Created by gkatzioura on 6/2/16.
 */
@Entity
@Table(name = "employee", schema="spring_data_jpa_example")
public class Employee {

    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
 
    @Column(name = "firstname")
    private String firstName;
 
    @Column(name = "lastname")
    private String lastname;
 
    @Column(name = "email")
    private String email;
 
    @Column(name = "age")
    private Integer age;
 

    @Column(name = "salary")
    private Integer salary;
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getFirstName() {
        return firstName;
    }
 
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
 
    public String getLastname() {
        return lastname;
    }
 
    public void setLastname(String lastname) {
        this.lastname = lastname;
    }
 
    public String getEmail() {
        return email;
    }
 
    public void setEmail(String email) {
        this.email = email;
    }
 
    public Integer getAge() {
        return age;
    }
 
    public void setAge(Integer age) {
        this.age = age;
    }
 
    public Integer getSalary() {
        return salary;
    }
 
    public void setSalary(Integer salary) {
        this.salary = salary;
    }
}
Репозиторий, который поможет нам выбрать всех работников
package com.gkatzioura.springdata.jpa.persistence.repository;
 
import com.gkatzioura.springdata.jpa.persistence.entity.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
 
/**
 * Created by gkatzioura on 6/2/16.
 */
@Repository

public interface EmployeeRepository extends JpaRepository{
}
И сам контроллер,который будет получать данные
package com.gkatzioura.springdata.jpa.controller;
 
import com.gkatzioura.springdata.jpa.persistence.entity.Employee;
import com.gkatzioura.springdata.jpa.persistence.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.List;
 
/**
 * Created by gkatzioura on 6/2/16.
 */
@RestController
public class TestController {
 
    @Autowired
    private EmployeeRepository employeeRepository;
 
    @RequestMapping("/employee")
    public List getTest() {
 
        return employeeRepository.findAll();
    }
}
Очень удобно, все зависимости и другие xml-конфигурации остались в прошлом.
Весь код можно найти здесь


PS это мой перевод данной статьи.

понедельник, 6 марта 2017 г.

Spring Dependency Lookup

Рассмотрим способ внедрения зависимостей через DependencyLookup. Предположим, нам необходимо получать новый инстанс объекта при каждом обращении. С этим поможет справится DependencyLookup. Способ реализации через аннотации. ApplicationContext будет практически пустой:

    

Интерфейс бина в котором необходим постоянно обновляемый бин:
package com.test.example;

public interface DemoBean {
    MyHelper getMyHelper();
    void someOperation();
}


Релизация постоянно обновляемого бина:
package com.test.example;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("prototype")
public class MyHelper {
    public void doSomethingUsefully(){

    }
}

Самое интересное это @Scope("prototype"). Данная аннотация говорит Spring, что, данный бин не синглтон. Реализация бина в который внедряется зависимость:

package com.test.example;

import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.stereotype.Component;

@Component
public class AbstractLookupDemoBean implements DemoBean{

    @Lookup
    public MyHelper getMyHelper(){
        return null;
    }

    public void someOperation() {
        getMyHelper().doSomethingUsefully();
    }
}

Аннотация @Lookup говорит, о том, что Spring'у необходимо будет взят данную функцию и предоставить свою реализацию с уже внедренной зависимостью, т.е. можно было бы сделать данный класс абстрактным и  сделать так:
@Lookup
public abstract MyHelper getMyHelper();

Работоспособность бы не изменилась. Ну и класс запуска:
package com.test.example;

import org.springframework.context.support.GenericXmlApplicationContext;

public class Application {
    public static void main(String[] arg){
        GenericXmlApplicationContext context = new GenericXmlApplicationContext();
        context.load("application.xml");
        context.refresh();

        DemoBean demoBean = (DemoBean) context.getBean("abstractLookupDemoBean");
        demoBean.someOperation();
        MyHelper helper1 = demoBean.getMyHelper();
        MyHelper helper2 = demoBean.getMyHelper();
        System.out.println("Helper Instances the Same?: " + (helper1 == helper2));
    }
}

В консоли всегда будет false
Теперь рассмотрим реализацию через xml. Все аннотации будут удалены, Контекст будет


    
    
        
    


Классы не изменятся, в них только пропадут аннотации.

вторник, 28 февраля 2017 г.

Вызов Rest сервиса в Java

Рассмотрим способо обращения к Rest-сервису, на примере микросервисов. Так же в скользь будет рассмотрен способ избежения дублирования кода в микросервисах. Я использую данную статью, позже, планирую ее перевести.

Первым делом реализуем библиотечную часть, которая будет использоваться во всех частях микросервиса для обмена.
package com.example.user.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by bartoszjedrzejewski on 08/06/2016.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserView {

    private long id;
    private String forename;
    private String surname;
    private String organisation;
    private List notifications;
    private long points;

    public UserView(){

    }

    public long getId() {
        return id;
    }

    public String getForename() {
        return forename;
    }

    public String getSurname() {
        return surname;
    }

    public String getOrganisation() {
        return organisation;
    }

    public List getNotifications() {
        return notifications;
    }

    public long getPoints() {
        return points;
    }
}

и pom-файл для мавена:

    4.0.0

    com.example
    user-client-libs
    0.0.1-SNAPSHOT
    
        
            com.fasterxml.jackson.core
            jackson-annotations
            2.6.6
        
    



Чтобы была возможность использовать данный класс, необходимо выполнить в консоли mvn clean install


Теперь рассмотрим микросервис, предоставляющий информацию о пользователе.

Пользователь:
package com.example;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by bartoszjedrzejewski on 08/06/2016.
 */
public class User {

    private final long id;
    private final String forename;
    private final String surname;
    private final String organisation;
    private final List notifications;
    private final long points;
    //Friends are deprecated and should not be used
    private final List friends;

    public User(int id) {
        String[] forenames = {"Alice", "Manjula", "Bartosz", "Mack"};
        String[] surnames = {"Smith", "Salvatore", "Jedrzejewski", "Scott"};
        String[] organisations = {"ScottLogic", "UNICEF"};

        forename = forenames[id%3];
        surname = surnames[id%4];
        organisation = organisations[id%2];
        notifications= new ArrayList<>();
        notifications.add("You have been promoted!");
        notifications.add("Sorry, disregard the previous notifaction- wrong user");
        points = id * 31 % 1000;

        //You have no friends
        friends = new ArrayList<>();

        this.id = id;
    }

    public long getId() {
        return id;
    }

    public String getForename() {
        return forename;
    }

    public String getSurname() {
        return surname;
    }

    public String getOrganisation() {
        return organisation;
    }

    public List getNotifications() {
        return notifications;
    }

    public long getPoints() {
        return points;
    }

    public List getFriends() {
        return friends;
    }
}

Обычный Rest-контроллер:
package com.example;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.atomic.AtomicLong;

/**
 * Created by bartoszjedrzejewski on 08/06/2016.
 */
@RestController
public class UserController {

    @RequestMapping("/user")
    public User getUser(@RequestParam(value="id", defaultValue="1") int id) {
        return new User(id);
    }

}
И запускалочка от SpringBoot:
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserServiceApplication {

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

Для настройки порта используется файл application.properties.Укажем в нем: server.port = 9001

pom-файл:

 4.0.0

 com.example
 user-service
 0.0.1-SNAPSHOT
 jar

 user-service
 Demo user-service with Spring Boot

 
  org.springframework.boot
  spring-boot-starter-parent
  1.3.5.RELEASE
   
 

 
  UTF-8
  1.8
 

 
  
   org.springframework.boot
   spring-boot-starter-web
  
  
   org.springframework.boot
   spring-boot-starter-test
   test
  
  
   com.fasterxml.jackson.core
   jackson-databind
   2.5.0
  
 


 
  
   
    org.springframework.boot
    spring-boot-maven-plugin
   
  
 
 




Простенький Rest-контроллер, который обращается к выше описанному микросервису:
package com.example;

import com.example.user.dto.UserView;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * Created by bartoszjedrzejewski on 09/06/2016.
 */
@RestController
public class UserDashboardController {

    @RequestMapping("/dashboard")
    public String getUser(@RequestParam(value="id", defaultValue="1") int id) {
        RestTemplate restTemplate = new RestTemplate();
        UserView user = restTemplate.getForObject("http://localhost:9001/user?id="+id, UserView.class);
        return "USER DASHBOARD 
" +
                "Welcome " + user.getForename() +" "+user.getSurname()+"
"+
                "You have " +user.getPoints() + " points! Good job!
"+
                "
"+
                "
"+user.getOrganisation();
    }

}
Запускалочка:
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserDashboardApplication {

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

порт: server.port = 9002

pom-файл:

 4.0.0

 com.example
 user-dashboard
 0.0.1-SNAPSHOT
 jar

 user-dashboard
 Demo user-dashboard service with Spring Boot

 
  org.springframework.boot
  spring-boot-starter-parent
  1.3.5.RELEASE
   
 

 
  UTF-8
  1.8
 

 
  
   org.springframework.boot
   spring-boot-starter-web
  
  
   org.springframework.boot
   spring-boot-starter-test
   test
  
  
   com.example
   user-client-libs
   0.0.1-SNAPSHOT
  
 
 
 
  
   
    org.springframework.boot
    spring-boot-maven-plugin
   
  
 
 



Вот и все, что нужно для обращения к Rest сервису. Все за нас делает объект RestTemplate. Конечно же можно выполнять не только Get запросы, но и post (postForObject(URI url, Object request, Class responseType )), put, delete. Подробности здесь.

воскресенье, 5 февраля 2017 г.

Кастомные аннотации в спринг

Попробуем написать свою простенькую аннтацию для спринга. Она будет выводить аргументы вызываемой функции. Подопытным классом будет калькулятор, который умеет складывать и вычитать два числа, вот его интерфейс:

package com.calculator;

public interface Calculator {
    int sum(int a, int b);
    int sub(int a, int b);
}


И его реализация:
package com.calculator;
import org.springframework.stereotype.Component;

@Component
@Profiling
public class CalculatorImpl implements Calculator {
    @Override
    public int sum(int a, int b) {
        return a + b;
    }

    @Override
    public int sub(int a, int b) {
        return a - b;
    }
}


Нас интересует новая аннотация @Profiling, которую мы и будем рализовывать. Для этого создадим следующий интерфейс:

package com.calculator;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Profiling {
}

Если у аннотации должны быть какие то параметры - создаются обычные филды, но это не наш случай. Так же необходимо обратить внимание на аннотацию @Retention(RetentionPolicy.RUNTIME) . Ее основное назначение - указать, как долго необходимо хранить данную аннотацию. Бывают следующие значния:

  • RetentionPolicy.SOURCE - аннотация используется на этапе компиляции и должна отбрасываться компилятором.
  • RetentionPolicy.CLASS - аннтоация будет записана в class-файл компилятором, но не должна быть доступна во время выполнения.
  • RetentionPolicy.RUNTIME - аннотация будет записана в class-файл и доступна во время выполнения.
Для данной аннотации значение по умолчанию CLASS. Для нас подходит только RUNTIME. 
Аннотация готова, но про нее ничего не знает сам спринг. Чтобы их познакомить необходимо создать BeanPostProcessor, для обработки данной аннотации.

package com.calculator;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

@Component
public class ProfilingAnnotationBeanPostProcessor implements BeanPostProcessor {
    private Map<String, Class>map = new HashMap<>();
    @Override
    public Object postProcessBeforeInitialization(Object bean, String s) throws BeansException {
        if (bean.getClass().isAnnotationPresent(Profiling.class)){
            map.put(s,bean.getClass());
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String s) throws BeansException {
        Class beanClass = map.get(s);
        if (beanClass != null){
            return Proxy.newProxyInstance(beanClass.getClassLoader(), beanClass.getInterfaces(),
                    (proxy, method, args) -> {
                        System.out.println("method:" + method.getName());
                        System.out.println("params:" + Arrays.toString(args));

                        return method.invoke(bean, args);
            });
        }
        return bean;
    }
}

в BeforeInitialization мы запоминаем те бины, которые помечены аннотацией, а в AfterInitialization, проверяем, если это запомненный бин, добавляем логирование. Ну и осталось создать контекст спринга, в котором нет ничего особенного:



и запуск:
package com;

import com.calculator.Calculator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;


public class App2 {
    public static void main(String[] arg){
        ApplicationContext context = new GenericXmlApplicationContext("context.xml");
        Calculator calc = context.getBean(Calculator.class);
        System.out.println(calc.sum(5,4));
        System.out.println(calc.sub(5,4));

    }


}

следующий вовод:
Connected to the target VM, address: '127.0.0.1:61942', transport: 'socket'
log4j:WARN No appenders could be found for logger (org.springframework.core.env.StandardEnvironment).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
Disconnected from the target VM, address: '127.0.0.1:61942', transport: 'socket'
method:sum
params:[5, 4]
9
method:sub
params:[5, 4]
1

Process finished with exit code 0


Process finished with exit code 0

воскресенье, 22 января 2017 г.

Структура Spring

Рассмотрим как происходит запуск контекста у Spring.

Превым делом должно быть какое-то описание для бинов, например xml. Для каждого описания есть BeanDefinitionReader, он парсит документ, складывает связку ид бина и бин дифинишинов. Далее Bean Factory из описания бинов первым делом создает бины, которые имплимитируют интерфейс BeanPostProcessor, с помощью этих бинов будет проиходить настройка бинов, отвечающих за бизнес-логику. Инициализация происходит следующим образом: каждый бин (только синглтон) проходит по всему списку BPP, далее вызывается инит метод у бина и бин второй раз проходит по списку BPP. На этом настройка бина завершена и он готов к использованию.

вторник, 8 ноября 2016 г.

@RestController vs. @Controller


Spring MVC и REST 

Spring - MVC фреймфорк, использующий аннотации, которые позволяют облегчить процесс создания RESTfull веб-сервиса. Основная разница между традиционным Spring MVC контроллером и RESTfull веб-сервис контроллером заключается в способе создания тела HTTP ответа. MVC контроллер опирается на технологию View, а RESTfull веб сервис контроллер  возвращает объект, который представляется в HTTP ответе  в виде JSON или XML. Для более подробного описания перейдите на эту ссылку.

Схема работы Spring MVC

  1. Клиент отправляет запрос к веб-сервису.
  2. Запрос перехватывается DispatcherServlet, который ищет Handler Mappings и соответствующий тип.
  3. Запрос обрабатывается контроллером и результат передается DispatcherServlet, а потом перенаправляется во view.

Использование @ResponseBody 

Когда вы используете аннотацию @ResponseBody для метода, Spring автоматически записывает результат в тело http ответа. Каждый метод в контроллере должен иметь данную аннотацию. Схема работы представлена на рисунке 1.
Рисунок 1.

Что происходит внутри

У Spring есть список HttpMessageConverters. HttpMessageConverter обязан конвертировать тело запроса к определенному классу и и класс к телу ответа, в зависимости от типа. Каждый раз, когда происходит запрос с аннотацией @ResponseBody, Spring ищет  среди всех HttpMessageConverters подходящий и использует его.

Пример

Рассмотрим POJO класс:

import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name = "Employee")
public class Employee {
    String name; 
    String email;
    public String getName() {
       return name;
    }
    public void setName(String name) {
      this.name = name;
    }
    public String getEmail() {
       return email;
    }
    public void setEmail(String email) {
      this.email = email;
    }
    public Employee() {
    } 
}
И класс с аннотацией @Controller:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.example.spring.model.Employee;
@Controller
@RequestMapping("employees")
public class EmployeeController {
    Employee employee = new Employee();
    @RequestMapping(value = "/{name}", method = RequestMethod.GET, produces = "application/json")
    public @ResponseBody Employee getEmployeeInJSON(@PathVariable String name) {
       employee.setName(name);
       employee.setEmail("employee1@genuitec.com");
    return employee; 
    }
    @RequestMapping(value = "/{name}.xml", method = RequestMethod.GET, produces = "application/xml")
    public @ResponseBody Employee getEmployeeInXML(@PathVariable String name) {
       employee.setName(name);
     employee.setEmail("employee1@genuitec.com");
       return employee; 
    }
}
В результате получим:
JSON: http://localhost:8080/SpringRestControllerExample/rest/employees/Bob

XML: http://localhost:8080/SpringRestControllerExample/rest/employees/Bob.xml

Использование аннотации @RestController

В Spring 4.0 была представлена аннотация @RestController. Применив ее к контроллеру добавляются аннотации @Controller, а так же @ResponseBody применяется ко всем методам. Подробнее можно почитать здесь. Схема работы на рисунке 2.
Рассмотрим этот же пример, но с новой аннотацией, POJO- класс не изменится, а контроллер примет следующий вид:

package com.example.spring.rest;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.example.spring.model.Employee;
@RestController
@RequestMapping("employees")
public class EmployeeController {
    Employee employee = new Employee();
    @RequestMapping(value = "/{name}", method = RequestMethod.GET, produces = "application/json")
    public Employee getEmployeeInJSON(@PathVariable String name) {
       employee.setName(name);
       employee.setEmail("employee1@genuitec.com");
       return employee;
    }
    @RequestMapping(value = "/{name}.xml", method = RequestMethod.GET, produces = "application/xml")
    public Employee getEmployeeInXML(@PathVariable String name) {
       employee.setName(name);
       employee.setEmail("employee1@genuitec.com");
    return employee; 
    } 
}
Заметьте, что не нужно добавлять @ResponseBody к каждому методу. Запустив приложение - получим тот же результат.

PS это мой  перевод данной статьи