Всички сме свидетели на нарастване на популярността на микросервизните архитектури. В архитектурата на микросервизите Dropwizard командва много важно място. Това е рамка за изграждане на RESTful уеб услуги или, по-точно, набор от инструменти и рамки за изграждане на RESTful уеб услуги.
Той позволява на разработчиците бързо стартиране на проекта. Това ви помага да опаковате приложенията си, за да можете лесно да ги разположите в производствена среда като самостоятелни услуги. Ако някога сте били в ситуация, в която трябва да стартирате проект в Пролетна рамка например, вероятно знаете колко болезнено може да бъде.
С Dropwizard е просто въпрос на добавяне на една зависимост на Maven.
В този блог ще ви преведа през целия процес на писане на проста услуга Dropwizard RESTful. След като приключим, ще имаме услуга за основни CRUD операции на „части“. Всъщност няма значение каква „част“ е; може да е всичко. Просто първо ми дойде наум.
Ще съхраняваме данните в база данни на MySQL, използвайки JDBI за тяхното заявяване и ще използваме следните крайни точки:
GET /parts
-за извличане на всички части от DBGET /part/{id}
за да получите определена част от DBPOST /parts
-за да създадете нова частPUT /parts/{id}
-да редактирате съществуваща частDELETE /parts/{id}
-за да изтриете частта от DBЩе използваме OAuth за удостоверяване на нашата услуга и накрая ще добавим някои модулни тестове към нея.
Вместо да включва всички библиотеки, необходими за изграждане на REST услуга поотделно и да конфигурира всяка от тях, Dropwizard прави това вместо нас. Ето списъка с библиотеки, които се предлагат с Dropwizard по подразбиране:
Освен горния списък, има много други библиотеки като Joda Time, Liquibase, Apache HTTP Client и Hibernate Validator, използвани от Dropwizard за изграждане на REST услуги.
Dropwizard официално поддържа Мейвън . Дори да можете да използвате други инструменти за изграждане, повечето ръководства и документация използват Maven, така че ще го използваме и тук. Ако не сте запознати с Maven, можете да проверите това Урок за Maven .
Това е първата стъпка в създаването на вашето приложение Dropwizard. Моля, добавете следния запис във вашия Maven’s pom.xml
файл:
io.dropwizard dropwizard-core ${dropwizard.version}
Преди да добавите горния запис, можете да добавите dropwizard.version
както по-долу:
1.1.0
Това е. Приключихте с написването на конфигурацията на Maven. Това ще изтегли всички необходими зависимости към вашия проект. Токът Версията на Dropwizard е 1.1.0 , така че ще го използваме в това ръководство.
Сега можем да преминем към писането на първото ни истинско приложение Dropwizard.
Dropwizard съхранява конфигурации в ЯМЛ файлове. Ще трябва да имате файла configuration.yml
в основната папка на вашето приложение. След това този файл ще бъде десериализиран до екземпляр от класа на конфигурацията на вашето приложение и ще бъде проверен. Конфигурационният файл на вашето приложение е подкласът на конфигурационния клас на Dropwizard (io.dropwizard.Configuration
).
Нека създадем прост клас за конфигуриране:
import javax.validation.Valid; import javax.validation.constraints.NotNull; import com.fasterxml.jackson.annotation.JsonProperty; import io.dropwizard.Configuration; import io.dropwizard.db.DataSourceFactory; public class DropwizardBlogConfiguration extends Configuration { private static final String DATABASE = 'database'; @Valid @NotNull private DataSourceFactory dataSourceFactory = new DataSourceFactory(); @JsonProperty(DATABASE) public DataSourceFactory getDataSourceFactory() { return dataSourceFactory; } @JsonProperty(DATABASE) public void setDataSourceFactory(final DataSourceFactory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; } }
YAML конфигурационният файл ще изглежда така:
database: driverClass: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost/dropwizard_blog user: dropwizard_blog password: dropwizard_blog maxWaitForConnection: 1s validationQuery: 'SELECT 1' validationQueryTimeout: 3s minSize: 8 maxSize: 32 checkConnectionWhileIdle: false evictionInterval: 10s minIdleTime: 1 minute checkConnectionOnBorrow: true
Горният клас ще бъде десериализиран от YAML файла и ще постави стойностите от YAML файла към този обект.
Сега трябва да отидем и да създадем основния клас на приложението. Този клас ще обедини всички пакети, ще приведе приложението и ще го стартира за употреба.
Ето пример за клас на приложение в Dropwizard:
import io.dropwizard.Application; import io.dropwizard.auth.AuthDynamicFeature; import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter; import io.dropwizard.setup.Environment; import javax.sql.DataSource; import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; import org.skife.jdbi.v2.DBI; import com.toptal.blog.auth.DropwizardBlogAuthenticator; import com.toptal.blog.auth.DropwizardBlogAuthorizer; import com.toptal.blog.auth.User; import com.toptal.blog.config.DropwizardBlogConfiguration; import com.toptal.blog.health.DropwizardBlogApplicationHealthCheck; import com.toptal.blog.resource.PartsResource; import com.toptal.blog.service.PartsService; public class DropwizardBlogApplication extends Application { private static final String SQL = 'sql'; private static final String DROPWIZARD_BLOG_SERVICE = 'Dropwizard blog service'; private static final String BEARER = 'Bearer'; public static void main(String[] args) throws Exception { new DropwizardBlogApplication().run(args); } @Override public void run(DropwizardBlogConfiguration configuration, Environment environment) { // Datasource configuration final DataSource dataSource = configuration.getDataSourceFactory().build(environment.metrics(), SQL); DBI dbi = new DBI(dataSource); // Register Health Check DropwizardBlogApplicationHealthCheck healthCheck = new DropwizardBlogApplicationHealthCheck(dbi.onDemand(PartsService.class)); environment.healthChecks().register(DROPWIZARD_BLOG_SERVICE, healthCheck); // Register OAuth authentication environment.jersey() .register(new AuthDynamicFeature(new OAuthCredentialAuthFilter.Builder() .setAuthenticator(new DropwizardBlogAuthenticator()) .setAuthorizer(new DropwizardBlogAuthorizer()).setPrefix(BEARER).buildAuthFilter())); environment.jersey().register(RolesAllowedDynamicFeature.class); // Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class))); } }
Това, което всъщност е направено по-горе, е замяна на метода за изпълнение на Dropwizard. В този метод ние създаваме инстанция на DB връзка, регистрираме нашата персонализирана проверка на състоянието (ще говорим за това по-късно), инициализираме OAuth удостоверяване за нашата услуга и накрая регистрираме ресурс на Dropwizard.
Всичко това ще бъде обяснено по-късно.
Сега трябва да започнем да мислим за нашия REST API и какво ще бъде представянето на нашия ресурс. Трябва да проектираме формата JSON и съответния клас на представяне, който преобразува в желания формат JSON.
Нека да разгледаме примерния JSON формат за този прост пример за клас на представяне:
{ 'code': 200, 'data': { 'id': 1, 'name': 'Part 1', 'code': 'PART_1_CODE' } }
За горния формат JSON бихме създали класа на представяне, както е показано по-долу:
import org.hibernate.validator.constraints.Length; import com.fasterxml.jackson.annotation.JsonProperty; public class Representation { private long code; @Length(max = 3) private T data; public Representation() { // Jackson deserialization } public Representation(long code, T data) { this.code = code; this.data = data; } @JsonProperty public long getCode() { return code; } @JsonProperty public T getData() { return data; } }
Това е доста просто POJO.
Ресурсът е това, което са REST услугите. Това не е нищо друго освен URI на крайна точка за достъп до ресурса на сървъра. В този пример ще имаме клас ресурси с няколко анотации за картографиране на URI на заявка. Тъй като Dropwizard използва внедряването на JAX-RS, ние ще дефинираме пътя на URI, използвайки @Path
анотация.
Ето един ресурсен клас за нашия пример за Dropwizard:
import java.util.List; import javax.annotation.security.RolesAllowed; import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.eclipse.jetty.http.HttpStatus; import com.codahale.metrics.annotation.Timed; import com.toptal.blog.model.Part; import com.toptal.blog.representation.Representation; import com.toptal.blog.service.PartsService; @Path('/parts') @Produces(MediaType.APPLICATION_JSON) @RolesAllowed('ADMIN') public class PartsResource { private final PartsService partsService;; public PartsResource(PartsService partsService) { this.partsService = partsService; } @GET @Timed public Representation getParts() { return new Representation(HttpStatus.OK_200, partsService.getParts()); } @GET @Timed @Path('{id}') public Representation getPart(@PathParam('id') final int id) { return new Representation(HttpStatus.OK_200, partsService.getPart(id)); } @POST @Timed public Representation createPart(@NotNull @Valid final Part part) { return new Representation(HttpStatus.OK_200, partsService.createPart(part)); } @PUT @Timed @Path('{id}') public Representation editPart(@NotNull @Valid final Part part, @PathParam('id') final int id) { part.setId(id); return new Representation(HttpStatus.OK_200, partsService.editPart(part)); } @DELETE @Timed @Path('{id}') public Representation deletePart(@PathParam('id') final int id) { return new Representation(HttpStatus.OK_200, partsService.deletePart(id)); } }
Можете да видите, че всички крайни точки всъщност са дефинирани в този клас.
Бих се върнал сега към основния клас на приложението. Можете да видите в края на този клас, че сме регистрирали нашия ресурс за инициализиране с изпълнението на услугата. Трябва да го направим с всички ресурси, които може да имаме в нашето приложение. Това е кодовият фрагмент, отговорен за това:
// Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class)));
За правилното боравене с изключения и възможността да бъдем независими от механизма за съхранение на данни, ще въведем клас на услуга „среден слой“. Това е класът, който ще извикаме от нашия ресурсен слой и не ни интересува какво е в основата. Ето защо имаме този слой между ресурсния и DAO слоевете. Ето нашия сервизен клас:
import java.util.List; import java.util.Objects; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response.Status; import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException; import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException; import org.skife.jdbi.v2.sqlobject.CreateSqlObject; import com.toptal.blog.dao.PartsDao; import com.toptal.blog.model.Part; public abstract class PartsService { private static final String PART_NOT_FOUND = 'Part id %s not found.'; private static final String DATABASE_REACH_ERROR = 'Could not reach the MySQL database. The database may be down or there may be network connectivity issues. Details: '; private static final String DATABASE_CONNECTION_ERROR = 'Could not create a connection to the MySQL database. The database configurations are likely incorrect. Details: '; private static final String DATABASE_UNEXPECTED_ERROR = 'Unexpected error occurred while attempting to reach the database. Details: '; private static final String SUCCESS = 'Success...'; private static final String UNEXPECTED_ERROR = 'An unexpected error occurred while deleting part.'; @CreateSqlObject abstract PartsDao partsDao(); public List getParts() { return partsDao().getParts(); } public Part getPart(int id) { Part part = partsDao().getPart(id); if (Objects.isNull(part)) { throw new WebApplicationException(String.format(PART_NOT_FOUND, id), Status.NOT_FOUND); } return part; } public Part createPart(Part part) { partsDao().createPart(part); return partsDao().getPart(partsDao().lastInsertId()); } public Part editPart(Part part) { if (Objects.isNull(partsDao().getPart(part.getId()))) { throw new WebApplicationException(String.format(PART_NOT_FOUND, part.getId()), Status.NOT_FOUND); } partsDao().editPart(part); return partsDao().getPart(part.getId()); } public String deletePart(final int id) { int result = partsDao().deletePart(id); switch (result) { case 1: return SUCCESS; case 0: throw new WebApplicationException(String.format(PART_NOT_FOUND, id), Status.NOT_FOUND); default: throw new WebApplicationException(UNEXPECTED_ERROR, Status.INTERNAL_SERVER_ERROR); } } public String performHealthCheck() { try { partsDao().getParts(); } catch (UnableToObtainConnectionException ex) { return checkUnableToObtainConnectionException(ex); } catch (UnableToExecuteStatementException ex) { return checkUnableToExecuteStatementException(ex); } catch (Exception ex) { return DATABASE_UNEXPECTED_ERROR + ex.getCause().getLocalizedMessage(); } return null; } private String checkUnableToObtainConnectionException(UnableToObtainConnectionException ex) { if (ex.getCause() instanceof java.sql.SQLNonTransientConnectionException) { return DATABASE_REACH_ERROR + ex.getCause().getLocalizedMessage(); } else if (ex.getCause() instanceof java.sql.SQLException) { return DATABASE_CONNECTION_ERROR + ex.getCause().getLocalizedMessage(); } else { return DATABASE_UNEXPECTED_ERROR + ex.getCause().getLocalizedMessage(); } } private String checkUnableToExecuteStatementException(UnableToExecuteStatementException ex) { if (ex.getCause() instanceof java.sql.SQLSyntaxErrorException) { return DATABASE_CONNECTION_ERROR + ex.getCause().getLocalizedMessage(); } else { return DATABASE_UNEXPECTED_ERROR + ex.getCause().getLocalizedMessage(); } } }
Последната част от него всъщност е изпълнение на проверка на състоянието, за което ще говорим по-късно.
Dropwizard поддържа JDBI и Hibernate. Това е отделен модул Maven, така че нека първо го добавим като зависимост, както и MySQL конектора:
io.dropwizard dropwizard-jdbi ${dropwizard.version} mysql mysql-connector-java ${mysql.connector.version}
За проста CRUD услуга аз лично предпочитам JDBI, тъй като тя е по-проста и много по-бърза за изпълнение. Създадох проста схема на MySQL с една таблица, която да се използва само в нашия пример. Можете да намерите скрипта за инициализация на схемата в източника. JDBI предлага просто писане на заявки, като използва анотации като @SqlQuery за четене и @SqlUpdate за запис на данни. Ето нашия интерфейс DAO:
import java.util.List; import org.skife.jdbi.v2.sqlobject.Bind; import org.skife.jdbi.v2.sqlobject.BindBean; import org.skife.jdbi.v2.sqlobject.SqlQuery; import org.skife.jdbi.v2.sqlobject.SqlUpdate; import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper; import com.toptal.blog.mapper.PartsMapper; import com.toptal.blog.model.Part; @RegisterMapper(PartsMapper.class) public interface PartsDao { @SqlQuery('select * from parts;') public List getParts(); @SqlQuery('select * from parts where id = :id') public Part getPart(@Bind('id') final int id); @SqlUpdate('insert into parts(name, code) values(:name, :code)') void createPart(@BindBean final Part part); @SqlUpdate('update parts set name = coalesce(:name, name), code = coalesce(:code, code) where id = :id') void editPart(@BindBean final Part part); @SqlUpdate('delete from parts where id = :id') int deletePart(@Bind('id') final int id); @SqlQuery('select last_insert_id();') public int lastInsertId(); }
Както можете да видите, това е доста просто. Трябва обаче да картографираме нашите набори от SQL резултати към модел, което правим, като регистрираме клас на mapper. Ето нашия клас за картографиране:
import java.sql.ResultSet; import java.sql.SQLException; import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.ResultSetMapper; import com.toptal.blog.model.Part; public class PartsMapper implements ResultSetMapper { private static final String ID = 'id'; private static final String NAME = 'name'; private static final String CODE = 'code'; public Part map(int i, ResultSet resultSet, StatementContext statementContext) throws SQLException { return new Part(resultSet.getInt(ID), resultSet.getString(NAME), resultSet.getString(CODE)); } }
И нашият модел:
import org.hibernate.validator.constraints.NotEmpty; public class Part { private int id; @NotEmpty private String name; @NotEmpty private String code; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public Part() { super(); } public Part(int id, String name, String code) { super(); this.id = id; this.name = name; this.code = code; } }
Dropwizard предлага естествена поддръжка за проверка на здравето. В нашия случай вероятно бихме искали да проверим дали базата данни работи и работи, преди да кажем, че нашата услуга е здрава. Това, което правим, всъщност е да извършим някои прости действия с DB, като получаване на части от DB и обработка на потенциалните резултати (успешни или изключения).
Ето нашето внедряване на проверка на състоянието в Dropwizard:
import com.codahale.metrics.health.HealthCheck; import com.toptal.blog.service.PartsService; public class DropwizardBlogApplicationHealthCheck extends HealthCheck { private static final String HEALTHY = 'The Dropwizard blog Service is healthy for read and write'; private static final String UNHEALTHY = 'The Dropwizard blog Service is not healthy. '; private static final String MESSAGE_PLACEHOLDER = '{}'; private final PartsService partsService; public DropwizardBlogApplicationHealthCheck(PartsService partsService) { this.partsService = partsService; } @Override public Result check() throws Exception { String mySqlHealthStatus = partsService.performHealthCheck(); if (mySqlHealthStatus == null) { return Result.healthy(HEALTHY); } else { return Result.unhealthy(UNHEALTHY + MESSAGE_PLACEHOLDER, mySqlHealthStatus); } } }
Dropwizard поддържа основно удостоверяване и OAuth . Тук. Ще ви покажа как да защитите вашата услуга с OAuth. Поради сложността обаче пропуснах основната структура на DB и просто показах как е обвита. Изпълнението в пълен мащаб не би трябвало да е проблем, започвайки от тук. Dropwizard има два важни интерфейса, които трябва да внедрим.
Първият е Удостоверител. Нашият клас трябва да приложи authenticate
метод, който трябва да провери дали даденият токен за достъп е валиден. Така че бих нарекъл това като първа порта към приложението. Ако успее, трябва да върне главница. Този принципал е нашият действителен потребител със своята роля. Ролята е важна за друг интерфейс на Dropwizard, който трябва да внедрим. Този е Authorizer и е отговорен за проверка дали потребителят има достатъчно разрешения за достъп до определен ресурс. Така че, ако се върнете назад и проверите нашия ресурсен клас, ще видите, че той изисква ролята на администратор за достъп до своите крайни точки. Тези пояснения също могат да бъдат за всеки метод. Поддръжката за оторизация на Dropwizard е отделен модул Maven, така че трябва да го добавим към зависимости:
io.dropwizard dropwizard-auth ${dropwizard.version}
Ето класовете от нашия пример, които всъщност не правят нищо умно, но това е скелет за пълномащабно OAuth оторизиране:
import java.util.Optional; import io.dropwizard.auth.AuthenticationException; import io.dropwizard.auth.Authenticator; public class DropwizardBlogAuthenticator implements Authenticator { @Override public Optional authenticate(String token) throws AuthenticationException { if ('test_token'.equals(token)) { return Optional.of(new User()); } return Optional.empty(); } }
import java.util.Objects; import io.dropwizard.auth.Authorizer; public class DropwizardBlogAuthorizer implements Authorizer { @Override public boolean authorize(User principal, String role) { // Allow any logged in user. if (Objects.nonNull(principal)) { return true; } return false; } }
import java.security.Principal; public class User implements Principal { private int id; private String username; private String password; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String getName() { return username; } }
Нека добавим някои модулни тестове към нашето приложение. Ще се придържам към тестването на специфични части на кода на Dropwizard, в нашия случай Представяне и ресурси. Ще трябва да добавим следните зависимости към нашия файл Maven:
io.dropwizard dropwizard-testing ${dropwizard.version} org.mockito mockito-core ${mockito.version} test
За тестване на представяне ще ни е необходим и примерен JSON файл, срещу който да тестваме. Така че нека създадем fixtures/part.json
под src/test/resources
:
{ 'id': 1, 'name': 'testPartName', 'code': 'testPartCode' }
И тук е JUnit тест клас:
import static io.dropwizard.testing.FixtureHelpers.fixture; import static org.assertj.core.api.Assertions.assertThat; import org.junit.Test; import com.fasterxml.jackson.databind.ObjectMapper; import com.toptal.blog.model.Part; import io.dropwizard.jackson.Jackson; public class RepresentationTest { private static final ObjectMapper MAPPER = Jackson.newObjectMapper(); private static final String PART_JSON = 'fixtures/part.json'; private static final String TEST_PART_NAME = 'testPartName'; private static final String TEST_PART_CODE = 'testPartCode'; @Test public void serializesToJSON() throws Exception { final Part part = new Part(1, TEST_PART_NAME, TEST_PART_CODE); final String expected = MAPPER.writeValueAsString(MAPPER.readValue(fixture(PART_JSON), Part.class)); assertThat(MAPPER.writeValueAsString(part)).isEqualTo(expected); } @Test public void deserializesFromJSON() throws Exception { final Part part = new Part(1, TEST_PART_NAME, TEST_PART_CODE); assertThat(MAPPER.readValue(fixture(PART_JSON), Part.class).getId()).isEqualTo(part.getId()); assertThat(MAPPER.readValue(fixture(PART_JSON), Part.class).getName()) .isEqualTo(part.getName()); assertThat(MAPPER.readValue(fixture(PART_JSON), Part.class).getCode()) .isEqualTo(part.getCode()); } }
Що се отнася до тестването на ресурси, основната точка на тестването на Dropwizard е, че всъщност се държите като HTTP клиент, изпращайки HTTP заявки срещу ресурси. Така че, вие не тествате методи, както обикновено в обичайния случай. Ето примера за нашите PartsResource
клас:
public class PartsResourceTest { private static final String SUCCESS = 'Success...'; private static final String TEST_PART_NAME = 'testPartName'; private static final String TEST_PART_CODE = 'testPartCode'; private static final String PARTS_ENDPOINT = '/parts'; private static final PartsService partsService = mock(PartsService.class); @ClassRule public static final ResourceTestRule resources = ResourceTestRule.builder().addResource(new PartsResource(partsService)).build(); private final Part part = new Part(1, TEST_PART_NAME, TEST_PART_CODE); @Before public void setup() { when(partsService.getPart(eq(1))).thenReturn(part); List parts = new ArrayList(); parts.add(part); when(partsService.getParts()).thenReturn(parts); when(partsService.createPart(any(Part.class))).thenReturn(part); when(partsService.editPart(any(Part.class))).thenReturn(part); when(partsService.deletePart(eq(1))).thenReturn(SUCCESS); } @After public void tearDown() { reset(partsService); } @Test public void testGetPart() { Part partResponse = resources.target(PARTS_ENDPOINT + '/1').request() .get(TestPartRepresentation.class).getData(); assertThat(partResponse.getId()).isEqualTo(part.getId()); assertThat(partResponse.getName()).isEqualTo(part.getName()); assertThat(partResponse.getCode()).isEqualTo(part.getCode()); verify(partsService).getPart(1); } @Test public void testGetParts() { List parts = resources.target(PARTS_ENDPOINT).request().get(TestPartsRepresentation.class).getData(); assertThat(parts.size()).isEqualTo(1); assertThat(parts.get(0).getId()).isEqualTo(part.getId()); assertThat(parts.get(0).getName()).isEqualTo(part.getName()); assertThat(parts.get(0).getCode()).isEqualTo(part.getCode()); verify(partsService).getParts(); } @Test public void testCreatePart() { Part newPart = resources.target(PARTS_ENDPOINT).request() .post(Entity.entity(part, MediaType.APPLICATION_JSON_TYPE), TestPartRepresentation.class) .getData(); assertNotNull(newPart); assertThat(newPart.getId()).isEqualTo(part.getId()); assertThat(newPart.getName()).isEqualTo(part.getName()); assertThat(newPart.getCode()).isEqualTo(part.getCode()); verify(partsService).createPart(any(Part.class)); } @Test public void testEditPart() { Part editedPart = resources.target(PARTS_ENDPOINT + '/1').request() .put(Entity.entity(part, MediaType.APPLICATION_JSON_TYPE), TestPartRepresentation.class) .getData(); assertNotNull(editedPart); assertThat(editedPart.getId()).isEqualTo(part.getId()); assertThat(editedPart.getName()).isEqualTo(part.getName()); assertThat(editedPart.getCode()).isEqualTo(part.getCode()); verify(partsService).editPart(any(Part.class)); } @Test public void testDeletePart() { assertThat(resources.target(PARTS_ENDPOINT + '/1').request() .delete(TestDeleteRepresentation.class).getData()).isEqualTo(SUCCESS); verify(partsService).deletePart(1); } private static class TestPartRepresentation extends Representation { } private static class TestPartsRepresentation extends Representation { } private static class TestDeleteRepresentation extends Representation { } }
Най-добрата практика е да се изгради един файл FAT JAR, който съдържа всички .class файлове, необходими за стартиране на вашето приложение. Един и същ JAR файл може да бъде разположен в различната среда от тестване до работна версия, без промяна в библиотеките на зависимостите. За да започнем да изграждаме нашето примерно приложение като дебел JAR, трябва да конфигурираме плъгин Maven, наречен maven-shadow. Трябва да добавите следните записи в раздела за приставки на вашия файл pom.xml.
Ето примерната конфигурация на Maven за изграждане на JAR файла.
4.0.0 com.endava dropwizard-blog 0.0.1-SNAPSHOT Dropwizard Blog example 1.1.0 2.7.12 6.0.6 1.8 1.8 io.dropwizard dropwizard-core ${dropwizard.version} io.dropwizard dropwizard-jdbi ${dropwizard.version} io.dropwizard dropwizard-auth ${dropwizard.version} io.dropwizard dropwizard-testing ${dropwizard.version} org.mockito mockito-core ${mockito.version} test mysql mysql-connector-java ${mysql.connector.version} org.apache.maven.plugins maven-shade-plugin 2.3 true *:* META-INF/*.SF META-INF/*.DSA META-INF/*.RSA package shade com.endava.blog.DropwizardBlogApplication
Сега трябва да можем да стартираме услугата. Ако успешно сте изградили своя JAR файл, всичко, което трябва да направите, е да отворите командния ред и просто да изпълните следната команда, за да изпълните вашия JAR файл:
java -jar target/dropwizard-blog-1.0.0.jar server configuration.yml
Ако всичко се оправи, ще видите нещо подобно:
INFO [2017-04-23 22:51:14,471] org.eclipse.jetty.util.log: Logging initialized @962ms to org.eclipse.jetty.util.log.Slf4jLog INFO [2017-04-23 22:51:14,537] io.dropwizard.server.DefaultServerFactory: Registering jersey handler with root path prefix: / INFO [2017-04-23 22:51:14,538] io.dropwizard.server.DefaultServerFactory: Registering admin handler with root path prefix: / INFO [2017-04-23 22:51:14,681] io.dropwizard.server.DefaultServerFactory: Registering jersey handler with root path prefix: / INFO [2017-04-23 22:51:14,681] io.dropwizard.server.DefaultServerFactory: Registering admin handler with root path prefix: / INFO [2017-04-23 22:51:14,682] io.dropwizard.server.ServerFactory: Starting DropwizardBlogApplication INFO [2017-04-23 22:51:14,752] org.eclipse.jetty.setuid.SetUIDListener: Opened [email protected] {HTTP/1.1,[http/1.1]}{0.0.0.0:8080} INFO [2017-04-23 22:51:14,752] org.eclipse.jetty.setuid.SetUIDListener: Opened [email protected] {HTTP/1.1,[http/1.1]}{0.0.0.0:8081} INFO [2017-04-23 22:51:14,753] org.eclipse.jetty.server.Server: jetty-9.4.2.v20170220 INFO [2017-04-23 22:51:15,153] io.dropwizard.jersey.DropwizardResourceConfig: The following paths were found for the configured resources: GET /parts (com.toptal.blog.resource.PartsResource) POST /parts (com.toptal.blog.resource.PartsResource) DELETE /parts/{id} (com.toptal.blog.resource.PartsResource) GET /parts/{id} (com.toptal.blog.resource.PartsResource) PUT /parts/{id} (com.toptal.blog.resource.PartsResource) INFO [2017-04-23 22:51:15,154] org.eclipse.jetty.server.handler.ContextHandler: Started [email protected] {/,null,AVAILABLE} INFO [2017-04-23 22:51:15,158] io.dropwizard.setup.AdminEnvironment: tasks = POST /tasks/log-level (io.dropwizard.servlets.tasks.LogConfigurationTask) POST /tasks/gc (io.dropwizard.servlets.tasks.GarbageCollectionTask) INFO [2017-04-23 22:51:15,162] org.eclipse.jetty.server.handler.ContextHandler: Started [email protected] {/,null,AVAILABLE} INFO [2017-04-23 22:51:15,176] org.eclipse.jetty.server.AbstractConnector: Started [email protected] {HTTP/1.1,[http/1.1]}{0.0.0.0:8080} INFO [2017-04-23 22:51:15,177] org.eclipse.jetty.server.AbstractConnector: Started [email protected] {HTTP/1.1,[http/1.1]}{0.0.0.0:8081} INFO [2017-04-23 22:51:15,177] org.eclipse.jetty.server.Server: Started @1670ms
Сега имате собствено приложение Dropwizard, което слуша на портове 8080 за заявки за приложения и 8081 за заявки за администрация.
Имайте предвид, че server configuration.yml
се използва за стартиране на HTTP сървъра и предаване на местоположението на YAML конфигурационния файл на сървъра.
Отлично! И накрая, внедрихме микроуслуга, използвайки Dropwizard framework. Сега да отидем на почивка и да изпием чаша чай. Направихте наистина добра работа.
Можете да използвате всеки HTTP клиент като POSTMAN или друг. Трябва да имате достъп до вашия сървър, като натиснете http://localhost:8080/parts
. Трябва да получите съобщение, че идентификационните данни са необходими за достъп до услугата. За удостоверяване добавете Authorization
заглавка с bearer test_token
стойност. Ако се направи успешно, трябва да видите нещо като:
{ 'code': 200, 'data': [] }
което означава, че вашата DB е празна. Създайте първата си част, като превключите HTTP метода от GET към POST и предоставете този полезен товар:
{ 'name':'My first part', 'code':'code_of_my_first_part' }
Всички останали крайни точки работят по същия начин, така че продължете да играете и се наслаждавайте.
По подразбиране приложението Dropwizard ще се стартира и работи в /
. Например, ако не споменавате нищо за контекстния път на приложението, по подразбиране приложението може да бъде достъпно от URL адреса http://localhost:8080/
Ако искате да конфигурирате свой собствен контекстен път за вашето приложение, моля, добавете следните записи към вашия YAML файл.
server: applicationContextPath: /application
Сега, когато вече разполагате с вашата услуга Dropwizard REST, нека обобщим някои ключови предимства или недостатъци на използването на Dropwizard като REST рамка. От тази публикация е абсолютно очевидно, че Dropwizard предлага изключително бързо стартиране на вашия проект. И това е може би най-голямото предимство на използването на Dropwizard.
Също така, той ще включва всички модерни библиотеки / инструменти, които някога ще са ви необходими при разработването на вашата услуга. Така че определено не е нужно да се притеснявате за това. Освен това ви дава много хубаво управление на конфигурацията. Разбира се, Dropwizard има и някои недостатъци. Използвайки Dropwizard, вие сте ограничени до използването на това, което Dropwizard предлага или поддържа. Губите част от свободата, с която може да сте свикнали, когато се развивате. Но все пак дори не бих го нарекъл недостатък, тъй като точно това прави Dropwizard това, което е - лесен за настройка, лесен за разработване, но все пак много здрава и високопроизводителна REST рамка.
Според мен добавянето на сложност към рамката чрез подкрепа на все повече и повече библиотеки на трети страни също би внесло ненужна сложност в развитието.
Представителният държавен трансфер (REST) е стил на архитектура (който не трябва да се смесва с набора от стандарти), базиран на набор от принципи, които описват как се определят и адресират мрежовите ресурси.
Крайната точка REST е изложена HTTP входна точка на REST услуга. В горния пример GET / части е една крайна точка, докато POST / части е друга.
Уеб услугата REST е всяко уеб приложение, което е структурирано следвайки архитектурния стил REST. Той слуша на HTTP или HTTPs порт за заявки и има своите крайни точки, изложени на клиента.
Това е архитектура на свободно свързани услуги. Тези услуги обаче заедно прилагат бизнес логиката на цялата система. Най-голямата полза от микроуслугите се крие във факта, че всяка малка услуга е по-лесна за разработване, разбиране и внедряване или поддръжка, без да се засяга останалата част от системата.