Статьи

Весенний ботинок с Лигой справедливости

Темные времена ожидают Лигу Справедливости, и грозный Темный Сейд собирается победить человеческий род. Бэтмен с помощью Wonder Woman находятся в поиске, чтобы собрать лигу вместе с одним критически важным аспектом. Надлежащая система управления членами Лиги Справедливости. Поскольку время не на их стороне, они не хотят проходить через громоздкий процесс создания проекта с нуля со всем необходимым. Бэтмен передает эту непростую задачу по созданию быстрой системы своему любимому доверенному Альфреду (поскольку Робин настолько непредсказуем), который говорит Бэтмену, что он вспоминает, что наткнулся на что-то под названием Spring Boot, которое помогает настроить все, что вам нужно, чтобы вы могли приступить к написанию кода для ваше приложение, а не суетиться с незначительными нюансами настройки конфигурации для вашего проекта. И вот он в это лезет. Давайте вернемся к этому с нашим любимым Альфредом, который будет использовать Spring Boot для создания системы управления членами Лиги Справедливости в кратчайшие сроки. Ну, по крайней мере, на данный момент, поскольку Бэтмену нравится иметь дело непосредственно с API REST.

Существует множество удобных способов настройки приложения Spring Boot. В этой статье мы сосредоточимся на традиционном способе загрузки пакета (Spring CLI) и настройке его с нуля в Ubuntu. Spring также поддерживает получение проекта, упакованного онлайн с помощью их инструмента . Вы можете скачать последнюю стабильную версию здесь . Для этого поста я использую релиз 1.3.0.M1.

После извлечения загруженного архива, во-первых, установите следующие параметры в своем профиле;

1
2
3
SPRING_BOOT_HOME=<extracted path>/spring-1.3.0.M1
 
PATH=$SPRING_BOOT_HOME/bin:$PATH

Впоследствии в вашем файле «bashrc» включите следующее;

1
. <extracted-path>/spring-1.3.0.M1/shell-completion/bash/spring

Последнее выполнение — это автоматическое завершение командной строки, когда вы работаете с spring-cli для создания приложений весенней загрузки. Пожалуйста, не забудьте «снабдить» как профиль, так и файлы «bashrc», чтобы изменения вступили в силу.

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

  • Весенний отдых
  • Spring Data
  • MongoDB

Итак, давайте начнем с создания шаблона проекта для приложения, выполнив следующую команду. Обратите внимание, что пример проекта может быть загружен из найденного хранилища GitHub.
здесь ;

1
spring init -dweb,data-mongodb,flapdoodle-mongo  --groupId com.justiceleague --artifactId justiceleaguemodule --build maven justiceleaguesystem

Это сгенерирует проект maven с Spring MVC и Spring Data с emebedded MongoDB.

По умолчанию spring-cli создает проект с именем, установленным как «Demo». Поэтому нам нужно будет переименовать соответствующий класс приложения. Если вы проверили источник из моего репозитория GitHub, упомянутого выше, то это будет сделано.

При загрузке Spring запуск приложения так же прост, как запуск файла jar, созданного проектом, который по существу вызывает приложение
класс, аннотированный @SpringBootApplication, который загружает Spring. Давайте посмотрим, как это выглядит;

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package com.justiceleague.justiceleaguemodule;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
/**
 * The main spring boot application which will start up a web container and wire
 * up all the required beans.
 *
 * @author dinuka
 *
 */
@SpringBootApplication
public class JusticeLeagueManagementApplication {
 
 public static void main(String[] args) {
  SpringApplication.run(JusticeLeagueManagementApplication.class, args);
 }
}

Затем мы переходим к нашим классам доменов, где мы используем spring-data вместе с mongodb для определения нашего уровня данных. Класс домена выглядит следующим образом;

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.justiceleague.justiceleaguemodule.domain;
 
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
 
/**
 * This class holds the details that will be stored about the justice league
 * members on MongoDB.
 *
 * @author dinuka
 *
 */
@Document(collection = "justiceLeagueMembers")
public class JusticeLeagueMemberDetail {
 
 @Id
 private ObjectId id;
 
 @Indexed
 private String name;
 
 private String superPower;
 
 private String location;
 
 public JusticeLeagueMemberDetail(String name, String superPower, String location) {
  this.name = name;
  this.superPower = superPower;
  this.location = location;
 }
 
 public String getId() {
  return id.toString();
 }
 
 public void setId(String id) {
  this.id = new ObjectId(id);
 }
 
 public String getName() {
  return name;
 }
 
 public void setName(String name) {
  this.name = name;
 }
 
 public String getSuperPower() {
  return superPower;
 }
 
 public void setSuperPower(String superPower) {
  this.superPower = superPower;
 }
 
 public String getLocation() {
  return location;
 }
 
 public void setLocation(String location) {
  this.location = location;
 }
 
}

Поскольку мы используем Spring-данные, они довольно интуитивно понятны, особенно если вы работаете с фоном JPA / Hibernate. Аннотации очень похожи. Единственной новой вещью будет аннотация @Document, которая обозначает название коллекции в нашей базе данных mongo. У нас также есть индекс, определенный для имени супергероя, так как больше запросов будет вращаться вокруг поиска по имени.

С Spring-data появилась возможность легко определять ваши репозитории, которые поддерживают обычные операции CRUD и некоторые операции чтения прямо из коробки без необходимости их записи. Таким образом, мы используем мощь хранилищ данных Spring в нашем приложении, и класс хранилища выглядит следующим образом;

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package com.justiceleague.justiceleaguemodule.dao;
 
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
 
import com.justiceleague.justiceleaguemodule.domain.JusticeLeagueMemberDetail;
 
public interface JusticeLeagueRepository extends MongoRepository<JusticeLeagueMemberDetail, String> {
 
 /**
  * This method will retrieve the justice league member details pertaining to
  * the name passed in.
  *
  * @param superHeroName
  *            the name of the justice league member to search and retrieve.
  * @return an instance of {@link JusticeLeagueMemberDetail} with the member
  *         details.
  */
 @Query("{ 'name' : {$regex: ?0, $options: 'i' }}")
 JusticeLeagueMemberDetail findBySuperHeroName(final String superHeroName);
}

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

Как видите, у нас определен только один метод. С помощью аннотации @Query мы пытаемся найти супергероя с пользователем регулярных выражений. Опция «i» означает, что мы должны игнорировать регистр при попытке найти совпадение в mongo db.

Затем мы переходим к реализации нашей логики хранения новых членов Лиги справедливости через наш уровень обслуживания.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.justiceleague.justiceleaguemodule.service.impl;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import com.justiceleague.justiceleaguemodule.constants.MessageConstants.ErrorMessages;
import com.justiceleague.justiceleaguemodule.dao.JusticeLeagueRepository;
import com.justiceleague.justiceleaguemodule.domain.JusticeLeagueMemberDetail;
import com.justiceleague.justiceleaguemodule.exception.JusticeLeagueManagementException;
import com.justiceleague.justiceleaguemodule.service.JusticeLeagueMemberService;
import com.justiceleague.justiceleaguemodule.web.dto.JusticeLeagueMemberDTO;
import com.justiceleague.justiceleaguemodule.web.transformer.DTOToDomainTransformer;
 
/**
 * This service class implements the {@link JusticeLeagueMemberService} to
 * provide the functionality required for the justice league system.
 *
 * @author dinuka
 *
 */
@Service
public class JusticeLeagueMemberServiceImpl implements JusticeLeagueMemberService {
 
 @Autowired
 private JusticeLeagueRepository justiceLeagueRepo;
 
 /**
  * {@inheritDoc}
  */
 public void addMember(JusticeLeagueMemberDTO justiceLeagueMember) {
  JusticeLeagueMemberDetail dbMember = justiceLeagueRepo.findBySuperHeroName(justiceLeagueMember.getName());
 
  if (dbMember != null) {
   throw new JusticeLeagueManagementException(ErrorMessages.MEMBER_ALREDY_EXISTS);
  }
  JusticeLeagueMemberDetail memberToPersist = DTOToDomainTransformer.transform(justiceLeagueMember);
  justiceLeagueRepo.insert(memberToPersist);
 }
 
}

Снова довольно тривиально, если член уже существует, мы выкидываем ошибку, иначе мы добавляем элемент. Здесь вы можете увидеть, что мы используем уже реализованные
вставить метод репозитория данных Spring, который мы только что определили.

Наконец, Альфред готов представить новую функциональность, которую он только что разработал, через REST API с использованием Spring REST, чтобы Бэтмен мог начать отправлять детали по HTTP, как он всегда путешествует.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.justiceleague.justiceleaguemodule.web.rest.controller;
 
import javax.validation.Valid;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
 
import com.justiceleague.justiceleaguemodule.constants.MessageConstants;
import com.justiceleague.justiceleaguemodule.service.JusticeLeagueMemberService;
import com.justiceleague.justiceleaguemodule.web.dto.JusticeLeagueMemberDTO;
import com.justiceleague.justiceleaguemodule.web.dto.ResponseDTO;
 
/**
 * This class exposes the REST API for the system.
 *
 * @author dinuka
 *
 */
@RestController
@RequestMapping("/justiceleague")
public class JusticeLeagueManagementController {
 
 @Autowired
 private JusticeLeagueMemberService memberService;
 
 /**
  * This method will be used to add justice league members to the system.
  *
  * @param justiceLeagueMember
  *            the justice league member to add.
  * @return an instance of {@link ResponseDTO} which will notify whether
  *         adding the member was successful.
  */
 @ResponseBody
 @ResponseStatus(value = HttpStatus.CREATED)
 @RequestMapping(method = RequestMethod.POST, path = "/addMember", produces = {
   MediaType.APPLICATION_JSON_VALUE }, consumes = { MediaType.APPLICATION_JSON_VALUE })
 public ResponseDTO addJusticeLeagueMember(@Valid @RequestBody JusticeLeagueMemberDTO justiceLeagueMember) {
  ResponseDTO responseDTO = new ResponseDTO(ResponseDTO.Status.SUCCESS,
    MessageConstants.MEMBER_ADDED_SUCCESSFULLY);
  try {
   memberService.addMember(justiceLeagueMember);
  } catch (Exception e) {
   responseDTO.setStatus(ResponseDTO.Status.FAIL);
   responseDTO.setMessage(e.getMessage());
  }
  return responseDTO;
 }
}

Мы представляем нашу функциональность как полезную нагрузку JSON, так как Бэтмену просто не хватает ее, хотя Альфред немного староват и иногда предпочитает XML.

Старик Альфред все еще хочет проверить свою функциональность, поскольку TDD — это просто его стиль. Итак, наконец, мы смотрим на интеграционные тесты, написанные Альфредом, чтобы убедиться, что первоначальная версия системы управления лигой правосудия работает так, как ожидалось. Обратите внимание, что здесь мы показываем только тесты API REST, хотя Альфред на самом деле рассказал больше о том, что вы можете проверить на
GitHub репо .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.justiceleague.justiceleaguemodule.test.util;
 
import java.io.IOException;
import java.net.UnknownHostException;
 
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.justiceleague.justiceleaguemodule.domain.JusticeLeagueMemberDetail;
 
import de.flapdoodle.embed.mongo.MongodExecutable;
import de.flapdoodle.embed.mongo.MongodStarter;
import de.flapdoodle.embed.mongo.config.IMongodConfig;
import de.flapdoodle.embed.mongo.config.MongodConfigBuilder;
import de.flapdoodle.embed.mongo.config.Net;
import de.flapdoodle.embed.mongo.distribution.Version;
 
/**
 * This class will have functionality required when running integration tests so
 * that invidivual classes do not need to implement the same functionality.
 *
 * @author dinuka
 *
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public abstract class BaseIntegrationTest {
 
 @Autowired
 protected MockMvc mockMvc;
 
 protected ObjectMapper mapper;
 
 private static MongodExecutable mongodExecutable;
 
 @Autowired
 protected MongoTemplate mongoTemplate;
 
 @Before
 public void setUp() {
  mapper = new ObjectMapper();
 }
 
 @After
 public void after() {
  mongoTemplate.dropCollection(JusticeLeagueMemberDetail.class);
 }
 
 /**
  * Here we are setting up an embedded mongodb instance to run with our
  * integration tests.
  *
  * @throws UnknownHostException
  * @throws IOException
  */
 @BeforeClass
 public static void beforeClass() throws UnknownHostException, IOException {
 
  MongodStarter starter = MongodStarter.getDefaultInstance();
 
  IMongodConfig mongoConfig = new MongodConfigBuilder().version(Version.Main.PRODUCTION)
    .net(new Net(27017, false)).build();
 
  mongodExecutable = starter.prepare(mongoConfig);
 
  try {
   mongodExecutable.start();
  } catch (Exception e) {
   closeMongoExecutable();
  }
 }
 
 @AfterClass
 public static void afterClass() {
  closeMongoExecutable();
 }
 
 private static void closeMongoExecutable() {
  if (mongodExecutable != null) {
   mongodExecutable.stop();
  }
 }
 
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package com.justiceleague.justiceleaguemodule.web.rest.controller;
 
import org.hamcrest.beans.SamePropertyValuesAs;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
 
import com.justiceleague.justiceleaguemodule.constants.MessageConstants;
import com.justiceleague.justiceleaguemodule.constants.MessageConstants.ErrorMessages;
import com.justiceleague.justiceleaguemodule.domain.JusticeLeagueMemberDetail;
import com.justiceleague.justiceleaguemodule.test.util.BaseIntegrationTest;
import com.justiceleague.justiceleaguemodule.web.dto.JusticeLeagueMemberDTO;
import com.justiceleague.justiceleaguemodule.web.dto.ResponseDTO;
import com.justiceleague.justiceleaguemodule.web.dto.ResponseDTO.Status;
 
/**
 * This class will test out the REST controller layer implemented by
 * {@link JusticeLeagueManagementController}
 *
 * @author dinuka
 *
 */
public class JusticeLeagueManagementControllerTest extends BaseIntegrationTest {
 
 /**
  * This method will test if the justice league member is added successfully
  * when valid details are passed in.
  *
  * @throws Exception
  */
 @Test
 public void testAddJusticeLeagueMember() throws Exception {
 
  JusticeLeagueMemberDTO flash = new JusticeLeagueMemberDTO("Barry Allen", "super speed", "Central City");
  String jsonContent = mapper.writeValueAsString(flash);
  String response = mockMvc
    .perform(MockMvcRequestBuilders.post("/justiceleague/addMember").accept(MediaType.APPLICATION_JSON)
      .contentType(MediaType.APPLICATION_JSON).content(jsonContent))
    .andExpect(MockMvcResultMatchers.status().isCreated()).andReturn().getResponse().getContentAsString();
 
  ResponseDTO expected = new ResponseDTO(Status.SUCCESS, MessageConstants.MEMBER_ADDED_SUCCESSFULLY);
  ResponseDTO receivedResponse = mapper.readValue(response, ResponseDTO.class);
 
  Assert.assertThat(receivedResponse, SamePropertyValuesAs.samePropertyValuesAs(expected));
 
 }
 
 /**
  * This method will test if an appropriate failure response is given when
  * the member being added already exists within the system.
  *
  * @throws Exception
  */
 @Test
 public void testAddJusticeLeagueMemberWhenMemberAlreadyExists() throws Exception {
  JusticeLeagueMemberDetail flashDetail = new JusticeLeagueMemberDetail("Barry Allen", "super speed",
    "Central City");
  mongoTemplate.save(flashDetail);
 
  JusticeLeagueMemberDTO flash = new JusticeLeagueMemberDTO("Barry Allen", "super speed", "Central City");
  String jsonContent = mapper.writeValueAsString(flash);
  String response = mockMvc
    .perform(MockMvcRequestBuilders.post("/justiceleague/addMember").accept(MediaType.APPLICATION_JSON)
      .contentType(MediaType.APPLICATION_JSON).content(jsonContent))
    .andExpect(MockMvcResultMatchers.status().isCreated()).andReturn().getResponse().getContentAsString();
 
  ResponseDTO expected = new ResponseDTO(Status.FAIL, ErrorMessages.MEMBER_ALREDY_EXISTS);
  ResponseDTO receivedResponse = mapper.readValue(response, ResponseDTO.class);
  Assert.assertThat(receivedResponse, SamePropertyValuesAs.samePropertyValuesAs(expected));
 }
 
 /**
  * This method will test if a valid client error is given if the data
  * required are not passed within the JSON request payload which in this
  * case is the super hero name.
  *
  * @throws Exception
  */
 @Test
 public void testAddJusticeLeagueMemberWhenNameNotPassedIn() throws Exception {
  // The super hero name is passed in as null here to see whether the
  // validation error handling kicks in.
  JusticeLeagueMemberDTO flash = new JusticeLeagueMemberDTO(null, "super speed", "Central City");
  String jsonContent = mapper.writeValueAsString(flash);
  mockMvc.perform(MockMvcRequestBuilders.post("/justiceleague/addMember").accept(MediaType.APPLICATION_JSON)
    .contentType(MediaType.APPLICATION_JSON).content(jsonContent))
    .andExpect(MockMvcResultMatchers.status().is4xxClientError());
 
 }
 
}

И это об этом. Благодаря возможности загрузки Spring, Альфред сумел получить минимальную систему управления лигой правосудия с REST API, которая была представлена ​​в кратчайшие сроки. В дальнейшем мы будем опираться на это приложение и посмотрим, как Альфред придет к тому, чтобы развернуть это приложение через докер на экземпляре Amazon AWS, управляемом Kubernetes, в ближайшее время. Впереди захватывающие времена, так что настройтесь.

Ссылка: Spring Boot с Лигой справедливости от нашего партнера JCG Динуки Арульератне в блоге My Journey Through IT .