суббота, 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. Тестирование web приложения

Рассмотрим процесс тестировани веб приложения. Для начала создадим тестовое веб приложение, очень похожее на пример из 1 части, только без БД.
Сущность:
package com.entity;

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

public class JournalEntry {
    private String title;
    private Date created;
    private String summary;
    private final SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy");
    public JournalEntry(String title, String summary, String date) throws
            ParseException{
        this.title = title;
        this.summary = summary;
        this.created = format.parse(date);
    }
    JournalEntry(){}
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public Date getCreated() {
        return created;
    }
    public void setCreated(String date) throws ParseException {
        Long _date = null;
        try{
            _date = Long.parseLong(date);
            this.created = new Date(_date);
            return;
        }catch(Exception ignored){}
        this.created = format.parse(date);
    }
    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }
    public String toString(){
        return "* JournalEntry(" + "Title: " +
                title +
                ",Summary: " +
                summary +
                ",Created: " +
                format.format(created) +
                ")";
    }
}
Контроллер:
package com.controller;

import com.entity.JournalEntry;
import org.springframework.web.bind.annotation.*;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@RestController
public class JournalController {
    private static List<JournalEntry> entries = new ArrayList<JournalEntry>();
    static {
        try {
            entries.add(new JournalEntry("Get to know Spring Boot", "Today I will learn Spring Boot", "01/01/2016"));
            entries.add(new JournalEntry("Simple Spring Boot Project", "I will do my first Spring Boot Project", "01/02/2016"));
            entries.add(new JournalEntry("Spring Boot Reading", "Read more about Spring Boot", "02/01/2016"));
            entries.add(new JournalEntry("Spring Boot in the Cloud", "Spring Boot using Cloud Foundry", "03/01/2016"));
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    @RequestMapping("/journal/all")
    public List<JournalEntry> getAll() throws ParseException{
        return entries;
    }

    @RequestMapping("/journal/findBy/title/{title}")
    public List<JournalEntry> findByTitleContains(@PathVariable String title) throws
            ParseException{
        return entries
                .stream()
                .filter(entry -> entry.getTitle().toLowerCase().contains(title.toLowerCase()))
                .collect(Collectors.toList());
    }

    @RequestMapping(value="/journal",method = RequestMethod.POST )
    public JournalEntry add(@RequestBody JournalEntry entry){
        entries.add(entry);
        return entry;
    }
}
Рассмотрим класс для тестирования:
package com;

import com.entity.JournalEntry;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;

import javax.annotation.Resource;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
@WebAppConfiguration
public class SpringBootApplicationTest {
    private final String SPRING_BOOT_MATCH = "Spring Boot";
    private final String CLOUD_MATCH = "Cloud";
    private HttpMessageConverter mappingJackson2HttpMessageConverter;
    private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
            MediaType.APPLICATION_JSON.getSubtype(),
            Charset.forName("utf8"));
    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webApplicationContext;
    @Autowired
    void setConverters(HttpMessageConverter<?>[] converters) {
        this.mappingJackson2HttpMessageConverter = Arrays.stream(converters).
                filter(
                        converter -> converter instanceof MappingJackson2HttpMessageConverter).
                findAny().get();
    }

    @Before
    public void setup() throws Exception {
        this.mockMvc = webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void getAll() throws Exception {
        mockMvc.perform(post("/journal")
                .content(this.toJsonString(new JournalEntry("Spring Boot Testing","Create Spring Boot Tests","05/09/2016")))
                .contentType(contentType)).andExpect(status().isOk());
        mockMvc.perform(get("/journal/all"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$",iterableWithSize(5)))
                .andExpect(jsonPath("$[0]['title']",containsString(SPRING_BOOT_MATCH)));
    }

    @Test
    public void findByTitle() throws Exception {
        mockMvc.perform(get("/journal/findBy/title/" + CLOUD_MATCH))
                .andExpect(status().isOk())
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$",iterableWithSize(1)))
                .andExpect(jsonPath("$[0]['title']",containsString(CLOUD_MATCH)));
    }

    @Test
    public void add() throws Exception {
        mockMvc.perform(post("/journal")
                .content(this.toJsonString(new JournalEntry("Spring Boot Testing","Create Spring Boot Tests","05/09/2016")))
                        .contentType(contentType)).andExpect(status().isOk());
    }

    @SuppressWarnings("unchecked")
    protected String toJsonString(Object obj) throws IOException {
        MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
        this.mappingJackson2HttpMessageConverter.write(obj, MediaType.APPLICATION_JSON,
                mockHttpOutputMessage);
        return mockHttpOutputMessage.getBodyAsString();
    }
}
После этого можно запускать тесты отдельно или все вместе, запустив сам класс SpringBootApplicationTest. Если запускать весь класс - нет возможности влиять на порядок запускаемых тестов, для этого придумана аннотация FixMethodOrde, передав туда MethodSorters.DEFAULT - методты будут запускать по порядку расположения в классе, MethodSorters.NAME_ASCENDING - по имени

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 - работают.