Memulai Dengan Layanan Mikro: Tutorial Dropwizard

Diterbitkan: 2022-03-11

Kita semua menyaksikan peningkatan popularitas arsitektur layanan mikro. Dalam arsitektur layanan mikro, Dropwizard memerintahkan tempat yang sangat penting. Ini adalah kerangka kerja untuk membangun layanan web RESTful atau, lebih tepatnya, seperangkat alat dan kerangka kerja untuk membangun layanan web RESTful.

Ini memungkinkan pengembang bootstrap proyek cepat. Ini membantu Anda mengemas aplikasi agar mudah diterapkan di lingkungan produksi sebagai layanan mandiri. Jika Anda pernah berada dalam situasi di mana Anda perlu mem-bootstrap proyek dalam kerangka Spring, misalnya, Anda mungkin tahu betapa menyakitkannya itu.

Ilustrasi: Contoh Layanan Mikro dalam Tutorial Dropwizard.

Dengan Dropwizard, hanya masalah menambahkan satu ketergantungan Maven.

Di blog ini, saya akan memandu Anda melalui proses lengkap penulisan layanan RESTful Dropwizard sederhana. Setelah selesai, kami akan memiliki layanan untuk operasi CRUD dasar pada "suku cadang". Tidak masalah apa "bagian" itu; itu bisa apa saja. Itu baru terlintas di pikiran dulu.

Kami akan menyimpan data dalam database MySQL, menggunakan JDBI untuk menanyakannya, dan akan menggunakan titik akhir berikut:

  • GET /parts -untuk mengambil semua bagian dari DB
  • GET /part/{id} untuk mendapatkan bagian tertentu dari DB
  • POST /parts -untuk membuat part baru
  • PUT /parts/{id} -untuk mengedit bagian yang sudah ada
  • DELETE /parts/{id} -untuk menghapus bagian dari DB

Kami akan menggunakan OAuth untuk mengautentikasi layanan kami, dan terakhir, menambahkan beberapa pengujian unit ke dalamnya.

Pustaka Dropwizard Default

Alih-alih menyertakan semua perpustakaan yang diperlukan untuk membangun layanan REST secara terpisah dan mengonfigurasinya masing-masing, Dropwizard melakukannya untuk kami. Berikut adalah daftar perpustakaan yang datang dengan Dropwizard secara default:

  • Dermaga: Anda akan memerlukan HTTP untuk menjalankan aplikasi web. Dropwizard menyematkan wadah servlet Jetty untuk menjalankan aplikasi web. Alih-alih menyebarkan aplikasi Anda ke server aplikasi atau server web, Dropwizard mendefinisikan metode utama yang memanggil server Jetty sebagai proses mandiri. Sampai sekarang, Dropwizard merekomendasikan hanya menjalankan aplikasi dengan Jetty; layanan web lain seperti Tomcat tidak didukung secara resmi.
  • Jersey: Jersey adalah salah satu implementasi REST API terbaik di pasaran. Juga, ini mengikuti spesifikasi JAX-RS standar, dan ini adalah implementasi referensi untuk spesifikasi JAX-RS. Dropwizard menggunakan Jersey sebagai kerangka kerja default untuk membangun aplikasi web RESTful.
  • Jackson: Jackson adalah standar de facto untuk penanganan format JSON. Ini adalah salah satu API pemetaan objek terbaik untuk format JSON.
  • Metrik: Dropwizard memiliki modul metriknya sendiri untuk mengekspos metrik aplikasi melalui titik akhir HTTP.
  • Guava: Selain struktur data yang tidak dapat diubah yang sangat dioptimalkan, Guava menyediakan semakin banyak kelas untuk mempercepat pengembangan di Java.
  • Logback dan Slf4j: Keduanya digunakan untuk mekanisme logging yang lebih baik.
  • Freemarker and Moustache: Memilih mesin template untuk aplikasi Anda adalah salah satu keputusan utama. Mesin template yang dipilih harus lebih fleksibel untuk menulis skrip yang lebih baik. Dropwizard menggunakan mesin template Freemarker dan Moustache yang terkenal dan populer untuk membangun antarmuka pengguna.

Selain daftar di atas, masih banyak library lain seperti Joda Time, Liquibase, Apache HTTP Client, dan Hibernate Validator yang digunakan Dropwizard untuk membangun layanan REST.

Konfigurasi Maven

Dropwizard secara resmi mendukung Maven. Bahkan jika Anda dapat menggunakan alat build lain, sebagian besar panduan dan dokumentasi menggunakan Maven, jadi kami akan menggunakannya juga di sini. Jika Anda tidak terbiasa dengan Maven, Anda bisa melihat tutorial Maven ini.

Ini adalah langkah pertama dalam membuat aplikasi Dropwizard Anda. Silakan tambahkan entri berikut di file pom.xml Maven Anda:

 <dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> </dependencies>

Sebelum menambahkan entri di atas, Anda bisa menambahkan dropwizard.version seperti di bawah ini:

 <properties> <dropwizard.version>1.1.0</dropwizard.version> </properties>

Itu dia. Anda selesai menulis konfigurasi Maven. Ini akan mengunduh semua dependensi yang diperlukan untuk proyek Anda. Versi Dropwizard saat ini adalah 1.1.0, jadi kami akan menggunakannya dalam panduan ini.

Sekarang, kita dapat melanjutkan untuk menulis aplikasi Dropwizard nyata pertama kita.

Tentukan Kelas Konfigurasi

Dropwizard menyimpan konfigurasi dalam file YAML. Anda harus memiliki file configuration.yml di folder root aplikasi Anda. File ini kemudian akan dideserialisasi ke instance kelas konfigurasi aplikasi Anda dan divalidasi. File konfigurasi aplikasi Anda adalah subkelas dari kelas konfigurasi Dropwizard ( io.dropwizard.Configuration ).

Mari kita buat kelas konfigurasi sederhana:

 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; } }

File konfigurasi YAML akan terlihat seperti ini:

 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

Kelas di atas akan di-deserialized dari file YAML dan menempatkan nilai dari file YAML ke objek ini.

Tentukan Kelas Aplikasi

Kita sekarang harus pergi dan membuat kelas aplikasi utama. Kelas ini akan menyatukan semua bundel dan membawa aplikasi dan membuatnya berjalan untuk digunakan.

Berikut adalah contoh kelas aplikasi di 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<DropwizardBlogConfiguration> { 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<User>() .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))); } }

Apa yang sebenarnya dilakukan di atas adalah mengganti metode run Dropwizard. Dalam metode ini, kami membuat instance koneksi DB, mendaftarkan pemeriksaan kesehatan kustom kami (kami akan membicarakannya nanti), menginisialisasi otentikasi OAuth untuk layanan kami, dan akhirnya, mendaftarkan sumber daya Dropwizard.

Semua ini akan dijelaskan nanti.

Tentukan Kelas Representasi

Sekarang kita harus mulai memikirkan REST API kita dan apa yang akan menjadi representasi dari resource kita. Kita harus mendesain format JSON dan kelas representasi yang sesuai yang mengonversi ke format JSON yang diinginkan.

Mari kita lihat contoh format JSON untuk contoh kelas representasi sederhana ini:

 { "code": 200, "data": { "id": 1, "name": "Part 1", "code": "PART_1_CODE" } }

Untuk format JSON di atas, kita akan membuat kelas representasi seperti di bawah ini:

 import org.hibernate.validator.constraints.Length; import com.fasterxml.jackson.annotation.JsonProperty; public class Representation<T> { 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; } }

Ini adalah POJO yang cukup sederhana.

Mendefinisikan Kelas Sumber Daya

Sumber daya adalah semua tentang layanan REST. Ini tidak lain adalah URI titik akhir untuk mengakses sumber daya di server. Dalam contoh ini, kita akan memiliki kelas sumber daya dengan beberapa anotasi untuk pemetaan URI permintaan. Karena Dropwizard menggunakan implementasi JAX-RS, kami akan mendefinisikan jalur URI menggunakan anotasi @Path .

Berikut adalah kelas sumber daya untuk contoh Dropwizard kami:

 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<List<Part>> getParts() { return new Representation<List<Part>>(HttpStatus.OK_200, partsService.getParts()); } @GET @Timed @Path("{id}") public Representation<Part> getPart(@PathParam("id") final int id) { return new Representation<Part>(HttpStatus.OK_200, partsService.getPart(id)); } @POST @Timed public Representation<Part> createPart(@NotNull @Valid final Part part) { return new Representation<Part>(HttpStatus.OK_200, partsService.createPart(part)); } @PUT @Timed @Path("{id}") public Representation<Part> editPart(@NotNull @Valid final Part part, @PathParam("id") final int id) { part.setId(id); return new Representation<Part>(HttpStatus.OK_200, partsService.editPart(part)); } @DELETE @Timed @Path("{id}") public Representation<String> deletePart(@PathParam("id") final int id) { return new Representation<String>(HttpStatus.OK_200, partsService.deletePart(id)); } }

Anda dapat melihat semua titik akhir sebenarnya didefinisikan di kelas ini.

Mendaftarkan Sumber Daya

Saya akan kembali sekarang ke kelas aplikasi utama. Anda dapat melihat di akhir kelas bahwa kami telah mendaftarkan sumber daya kami untuk diinisialisasi dengan menjalankan layanan. Kita perlu melakukannya dengan semua sumber daya yang mungkin kita miliki dalam aplikasi kita. Ini adalah cuplikan kode yang bertanggung jawab untuk itu:

 // Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class)));

Lapisan Layanan

Untuk penanganan pengecualian yang tepat dan kemampuan untuk tidak bergantung pada mesin penyimpanan data, kami akan memperkenalkan kelas layanan "lapisan menengah". Ini adalah kelas yang akan kita panggil dari lapisan sumber daya kita, dan kita tidak peduli apa yang mendasarinya. Itu sebabnya kami memiliki lapisan ini di antara lapisan sumber daya dan DAO. Berikut adalah kelas layanan kami:

 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<Part> 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(); } } }

Bagian terakhir sebenarnya adalah pelaksanaan pemeriksaan kesehatan, yang akan kita bicarakan nanti.

Lapisan DAO, JDBI, dan Mapper

Dropwizard mendukung JDBI dan Hibernate. Ini adalah modul Maven yang terpisah, jadi mari kita tambahkan terlebih dahulu sebagai dependensi serta konektor MySQL:

 <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-jdbi</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> </dependency>

Untuk layanan CRUD sederhana, saya pribadi lebih suka JDBI, karena lebih sederhana dan lebih cepat untuk diimplementasikan. Saya telah membuat skema MySQL sederhana dengan satu tabel hanya untuk digunakan dalam contoh kita. Anda dapat menemukan skrip init untuk skema di dalam sumber. JDBI menawarkan penulisan kueri sederhana dengan menggunakan anotasi seperti @SqlQuery untuk membaca dan @SqlUpdate untuk menulis data. Berikut adalah antarmuka DAO kami:

 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<Part> 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(); }

Seperti yang Anda lihat, ini cukup sederhana. Namun, kita perlu memetakan set hasil SQL kita ke model, yang kita lakukan dengan mendaftarkan kelas mapper. Berikut adalah kelas mapper kami:

 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<Part> { private static final String; 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)); } }

Dan model kami:

 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; } }

Pemeriksaan Kesehatan Dropwizard

Dropwizard menawarkan dukungan asli untuk pemeriksaan kesehatan. Dalam kasus kami, kami mungkin ingin memeriksa apakah database aktif dan berjalan sebelum mengatakan bahwa layanan kami sehat. Apa yang kami lakukan sebenarnya adalah melakukan beberapa tindakan DB sederhana seperti mendapatkan bagian dari DB dan menangani hasil potensial (berhasil atau pengecualian).

Berikut implementasi health check kami di 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); } } }

Menambahkan Otentikasi

Dropwizard mendukung otentikasi dasar dan OAuth. Di Sini. Saya akan menunjukkan cara melindungi layanan Anda dengan OAuth. Namun, karena kerumitannya, saya telah menghilangkan struktur DB yang mendasarinya dan hanya menunjukkan cara membungkusnya. Menerapkan ke skala penuh seharusnya tidak menjadi masalah mulai dari sini. Dropwizard memiliki dua antarmuka penting yang perlu kita implementasikan.

Yang pertama adalah Authenticator. Kelas kita harus mengimplementasikan metode authenticate , yang akan memeriksa apakah token akses yang diberikan valid. Jadi saya akan menyebutnya sebagai gerbang pertama ke aplikasi. Jika berhasil, itu harus mengembalikan kepala sekolah. Prinsipal ini adalah pengguna kami yang sebenarnya dengan perannya. Peran ini penting untuk antarmuka Dropwizard lain yang perlu kita implementasikan. Yang ini Authorizer, dan bertanggung jawab untuk memeriksa apakah pengguna memiliki izin yang cukup untuk mengakses sumber daya tertentu. Jadi, jika Anda kembali dan memeriksa kelas sumber daya kami, Anda akan melihat bahwa itu memerlukan peran admin untuk mengakses titik akhirnya. Anotasi ini bisa per metode juga. Dukungan otorisasi Dropwizard adalah modul Maven yang terpisah, jadi kami perlu menambahkannya ke dependensi:

 <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-auth</artifactId> <version>${dropwizard.version}</version> </dependency>

Berikut adalah kelas dari contoh kami yang sebenarnya tidak melakukan sesuatu yang cerdas, tetapi ini adalah kerangka untuk otorisasi OAuth skala penuh:

 import java.util.Optional; import io.dropwizard.auth.AuthenticationException; import io.dropwizard.auth.Authenticator; public class DropwizardBlogAuthenticator implements Authenticator<String, User> { @Override public Optional<User> 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<User> { @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; } }

Tes Unit di Dropwizard

Mari tambahkan beberapa unit test ke aplikasi kita. Saya akan tetap menguji Dropwizard bagian tertentu dari kode, dalam kasus kami Representasi dan Sumber Daya. Kita perlu menambahkan dependensi berikut ke file Maven kita:

 <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-testing</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>${mockito.version}</version> <scope>test</scope> </dependency>

Untuk menguji representasi, kami juga memerlukan contoh file JSON untuk diuji. Jadi mari kita buat fixtures/part.json di bawah src/test/resources :

 { "id": 1, "name": "testPartName", "code": "testPartCode" }

Dan inilah kelas tes 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()); } }

Ketika datang untuk menguji sumber daya, poin utama pengujian Dropwizard adalah bahwa Anda benar-benar berperilaku sebagai klien HTTP, mengirimkan permintaan HTTP terhadap sumber daya. Jadi, Anda tidak menguji metode seperti yang biasanya Anda lakukan dalam kasus umum. Berikut adalah contoh untuk kelas PartsResource kami:

 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<Part> 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<Part> 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<Part> { } private static class TestPartsRepresentation extends Representation<List<Part>> { } private static class TestDeleteRepresentation extends Representation<String> { } }

Bangun Aplikasi Dropwizard Anda

Praktik terbaik adalah membuat file FAT JAR tunggal yang berisi semua file .class yang diperlukan untuk menjalankan aplikasi Anda. File JAR yang sama dapat di-deploy ke lingkungan yang berbeda mulai dari pengujian hingga produksi tanpa perubahan apa pun di pustaka dependensi. Untuk mulai membangun aplikasi contoh kita sebagai JAR gemuk, kita perlu mengonfigurasi plugin Maven yang disebut maven-shade. Anda harus menambahkan entri berikut di bagian plugin dari file pom.xml Anda.

Berikut adalah contoh konfigurasi Maven untuk membangun file JAR.

 <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.endava</groupId> <artifactId>dropwizard-blog</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Dropwizard Blog example</name> <properties> <dropwizard.version>1.1.0</dropwizard.version> <mockito.version>2.7.12</mockito.version> <mysql.connector.version>6.0.6</mysql.connector.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-jdbi</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-auth</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-testing</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>${mockito.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.3</version> <configuration> <createDependencyReducedPom>true</createDependencyReducedPom> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" /> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.endava.blog.DropwizardBlogApplication</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>

Menjalankan Aplikasi Anda

Sekarang, kita harus dapat menjalankan layanan. Jika Anda telah berhasil membangun file JAR Anda, yang perlu Anda lakukan adalah membuka command prompt dan jalankan perintah berikut untuk mengeksekusi file JAR Anda:

 java -jar target/dropwizard-blog-1.0.0.jar server configuration.yml

Jika semua berjalan OK, maka Anda akan melihat sesuatu seperti ini:

 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 application@7d57dbb5{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 admin@630b6190{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 idjMutableServletContextHandler@58fa5769{/,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 idjMutableServletContextHandler@3fdcde7a{/,null,AVAILABLE} INFO [2017-04-23 22:51:15,176] org.eclipse.jetty.server.AbstractConnector: Started application@7d57dbb5{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 admin@630b6190{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

Now you have your own Dropwizard application listening on ports 8080 for application requests and 8081 for administration requests.

Note that server configuration.yml is used for starting the HTTP server and passing the YAML configuration file location to the server.

Bagus sekali! Finally, we have implemented a microservice using Dropwizard framework. Now let's go for a break and have a cup of tea. You have done really good job.

Accessing Resources

You can use any HTTP client like POSTMAN or any else. You should be able to access your server by hitting http://localhost:8080/parts . You should be receiving a message that the credentials are required to access the service. To authenticate, add Authorization header with bearer test_token value. If done successfully, you should see something like:

 { "code": 200, "data": [] }

meaning that your DB is empty. Create your first part by switching HTTP method from GET to POST, and supply this payload:

 { "name":"My first part", "code":"code_of_my_first_part" }

All other endpoints work in the same manner, so keep playing and enjoy.

How to Change Context Path

By default, Dropwizard application will start and running in the / . For example, if you are not mentioning anything about the context path of the application, by default, the application can be accessed from the URL http://localhost:8080/ . If you want to configure your own context path for your application, then please add the following entries to your YAML file.

 server: applicationContextPath: /application

Wrapping up our Dropwizard Tutorial

Now when you have your Dropwizard REST service up and running, let's summarize some key advantages or disadvantages of using Dropwizard as a REST framework. It's absolutely obvious from this post that Dropwizard offers extremely fast bootstrap of your project. And that's probably the biggest advantage of using Dropwizard.

Also, it will include all the cutting-edge libraries/tools that you will ever need in developing your service. So you definitely do not need to worry about that. It also gives you very nice configuration management. Of course, Dropwizard has some disadvantages as well. By using Dropwizard, you're kind of restricted to using what Dropwizard offers or supports. You lose some of the freedom you may be used to when developing. But still, I wouldn't even call it a disadvantage, as this is exactly what makes Dropwizard what it is—easy to set up, easy to develop, but yet a very robust and high-performance REST framework.

In my opinion, adding complexity to the framework by supporting more and more third party libraries would also introduce unnecessary complexity in development.