ابدأ مع Microservices: برنامج تعليمي Dropwizard

نشرت: 2022-03-11

نشهد جميعًا ارتفاعًا في شعبية بنى الخدمات المصغرة. في بنية الخدمات المصغرة ، يحتل Dropwizard مكانًا مهمًا للغاية. إنه إطار عمل لبناء خدمات ويب RESTful أو ، بشكل أكثر دقة ، مجموعة من الأدوات والأطر لبناء خدمات الويب RESTful.

يسمح للمطورين بإقلاع المشاريع بسرعة. يساعدك هذا في حزم تطبيقاتك لتكون قابلة للنشر بسهولة في بيئة إنتاج كخدمات قائمة بذاتها. إذا كنت في أي وقت مضى في موقف تحتاج فيه إلى تمهيد مشروع في إطار الربيع ، على سبيل المثال ، فربما تعرف مدى الألم الذي يمكن أن يكون عليه.

رسم توضيحي: مثال على الخدمات المصغرة في دروس Dropwizard.

مع Dropwizard ، إنها مجرد مسألة إضافة تبعية Maven واحدة.

في هذه المدونة ، سأوجهك خلال العملية الكاملة لكتابة خدمة Dropwizard RESTful بسيطة. بعد أن ننتهي ، سيكون لدينا خدمة لعمليات CRUD الأساسية على "الأجزاء". لا يهم حقًا ما هو "الجزء" ؛ يمكن أن يكون أي شيء. لقد خطر ببالي أولا

سنخزن البيانات في قاعدة بيانات MySQL ، باستخدام JDBI للاستعلام عنها ، وسنستخدم نقاط النهاية التالية:

  • GET /parts - لاسترداد جميع الأجزاء من DB
  • GET /part/{id} للحصول على جزء معين من DB
  • POST /parts -لإنشاء جزء جديد
  • PUT /parts/{id} -لتحرير جزء موجود
  • DELETE /parts/{id} -لحذف الجزء من قاعدة بيانات

سنستخدم OAuth لمصادقة خدمتنا ، وأخيرًا ، نضيف بعض اختبارات الوحدة إليها.

مكتبات Dropwizard الافتراضية

بدلاً من تضمين جميع المكتبات اللازمة لإنشاء خدمة REST بشكل منفصل وتكوين كل منها ، يقوم Dropwizard بذلك نيابةً عنا. فيما يلي قائمة المكتبات التي تأتي مع Dropwizard بشكل افتراضي:

  • Jetty: قد تحتاج إلى HTTP لتشغيل تطبيق ويب. يقوم Dropwizard بتضمين حاوية Jetty servlet لتشغيل تطبيقات الويب. بدلاً من نشر تطبيقاتك على خادم تطبيق أو خادم ويب ، يحدد Dropwizard طريقة رئيسية تستدعي خادم Jetty كعملية قائمة بذاتها. اعتبارًا من الآن ، يوصي Dropwizard بتشغيل التطبيق مع Jetty فقط ؛ خدمات الويب الأخرى مثل Tomcat غير مدعومة رسميًا.
  • جيرسي: جيرسي هي واحدة من أفضل تطبيقات REST API في السوق. كما أنه يتبع مواصفات JAX-RS القياسية ، وهو التطبيق المرجعي لمواصفات JAX-RS. يستخدم Dropwizard جيرسي كإطار عمل افتراضي لبناء تطبيقات ويب RESTful.
  • جاكسون: جاكسون هو المعيار الفعلي للتعامل مع تنسيق JSON. إنها واحدة من أفضل واجهات برمجة تطبيقات مخطط الكائنات لتنسيق JSON.
  • المقاييس: يحتوي Dropwizard على وحدة مقاييس خاصة به لعرض مقاييس التطبيق من خلال نقاط نهاية HTTP.
  • الجوافة: بالإضافة إلى هياكل البيانات الثابتة المحسنة للغاية ، توفر الجوافة عددًا متزايدًا من الفئات لتسريع التطوير في Java.
  • Logback و Slf4j: يستخدم هذان النوعان من أجل آليات تسجيل أفضل.
  • Freemarker and Moustache: يعد اختيار محركات النموذج لتطبيقك أحد القرارات الرئيسية. يجب أن يكون محرك القوالب المختار أكثر مرونة لكتابة نصوص أفضل. يستخدم Dropwizard محركات قوالب معروفة وشائعة Freemarker و Moustache لبناء واجهات المستخدم.

بصرف النظر عن القائمة أعلاه ، هناك العديد من المكتبات الأخرى مثل Joda Time و Liquibase و Apache HTTP Client و Hibernate Validator التي يستخدمها Dropwizard لبناء خدمات REST.

تكوين المخضرم

يدعم Dropwizard رسميًا Maven. حتى إذا كان بإمكانك استخدام أدوات إنشاء أخرى ، فإن معظم الأدلة والوثائق تستخدم Maven ، لذلك سنستخدمها هنا أيضًا. إذا لم تكن معتادًا على Maven ، فيمكنك الاطلاع على هذا البرنامج التعليمي Maven.

هذه هي الخطوة الأولى في إنشاء تطبيق Dropwizard الخاص بك. الرجاء إضافة الإدخال التالي في ملف pom.xml الخاص بـ Maven:

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

قبل إضافة الإدخال أعلاه ، يمكنك إضافة الإصدار dropwizard.version على النحو التالي:

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

هذا هو. لقد انتهيت من كتابة تكوين Maven. سيؤدي هذا إلى تنزيل جميع التبعيات المطلوبة لمشروعك. الإصدار الحالي من Dropwizard هو 1.1.0 ، لذلك سنستخدمه في هذا الدليل.

الآن ، يمكننا الانتقال إلى كتابة أول تطبيق حقيقي لـ Dropwizard.

تحديد فئة التكوين

يخزن Dropwizard التكوينات في ملفات YAML. سوف تحتاج إلى أن يكون لديك ملف 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<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))); } }

ما تم فعله أعلاه هو تجاوز طريقة تشغيل Dropwizard. في هذه الطريقة ، نقوم بإنشاء اتصال قاعدة بيانات ، وتسجيل فحصنا الصحي المخصص (سنتحدث عنه لاحقًا) ، وتهيئة مصادقة 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<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; } }

هذا هو بوجو بسيط إلى حد ما.

تحديد فئة الموارد

المورد هو ما تدور حوله خدمات 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<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)); } }

يمكنك أن ترى أن جميع نقاط النهاية محددة بالفعل في هذه الفئة.

تسجيل مورد

سأعود الآن إلى فئة التطبيق الرئيسية. يمكنك أن ترى في نهاية هذا الفصل أننا سجلنا موردنا ليتم تهيئته مع تشغيل الخدمة. نحتاج إلى القيام بذلك بكل الموارد التي قد تكون لدينا في تطبيقنا. هذا هو مقتطف الشفرة المسؤول عن ذلك:

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

الجزء الأخير منه هو في الواقع تنفيذ الفحص الصحي ، والذي سنتحدث عنه لاحقًا.

طبقة DAO و JDBI و Mapper

يدعم Dropwizard JDBI و Hibernate. إنها وحدة Maven منفصلة ، لذا دعنا نضيفها أولاً كتبعية بالإضافة إلى موصل 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>

للحصول على خدمة 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<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(); }

كما ترى ، الأمر بسيط إلى حد ما. ومع ذلك ، نحتاج إلى تعيين مجموعات نتائج SQL الخاصة بنا إلى نموذج ، وهو ما نقوم به من خلال تسجيل فئة معين. هنا فئة مصمم الخرائط لدينا:

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

ونموذجنا:

 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

يقدم Dropwizard دعمًا أصليًا للفحص الصحي. في حالتنا ، ربما نرغب في التحقق مما إذا كانت قاعدة البيانات قيد التشغيل قبل أن نقول إن خدمتنا صحية. ما نقوم به في الواقع هو تنفيذ بعض إجراءات قاعدة البيانات البسيطة مثل الحصول على أجزاء من قاعدة البيانات والتعامل مع النتائج المحتملة (الناجحة أو الاستثناءات).

إليك تنفيذ الفحص الصحي في 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. ومع ذلك ، نظرًا للتعقيد ، فقد حذفت بنية قاعدة بيانات أساسية وأظهرت للتو كيف يتم تغليفها. يجب ألا يكون التنفيذ على نطاق واسع مشكلة تبدأ من هنا. يحتوي Dropwizard على واجهتين مهمتين نحتاج إلى تنفيذهما.

الأول هو Authenticator. يجب أن يقوم صنفنا بتنفيذ طريقة authenticate ، والتي يجب أن تتحقق مما إذا كان رمز الوصول المحدد صالحًا. لذلك أود أن أسمي هذا كبوابة أولى للتطبيق. إذا نجحت ، يجب أن تعيد رأس المال. هذا المبدأ هو مستخدمنا الفعلي مع دوره. الدور مهم لواجهة Dropwizard أخرى نحتاج إلى تنفيذها. هذا هو Authorizer ، وهو مسؤول عن التحقق مما إذا كان المستخدم لديه أذونات كافية للوصول إلى مورد معين. لذلك ، إذا عدت وتحققت من فئة الموارد الخاصة بنا ، فسترى أنها تتطلب دور المسؤول للوصول إلى نقاط النهاية الخاصة بها. يمكن أن تكون هذه التعليقات التوضيحية لكل طريقة أيضًا. دعم تفويض Dropwizard عبارة عن وحدة Maven منفصلة ، لذلك نحتاج إلى إضافته إلى التبعيات:

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

في ما يلي الفئات من مثالنا التي لا تفعل شيئًا ذكيًا في الواقع ، لكنها هيكل عظمي لترخيص OAuth واسع النطاق:

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

اختبارات الوحدة في Dropwizard

دعنا نضيف بعض اختبارات الوحدة إلى تطبيقنا. سألتزم باختبار أجزاء معينة من الكود في Dropwizard ، في حالتنا التمثيل والموارد. سنحتاج إلى إضافة التبعيات التالية إلى ملف Maven الخاص بنا:

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

لاختبار التمثيل ، سنحتاج أيضًا إلى نموذج ملف 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<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> { } }

قم ببناء تطبيق Dropwizard الخاص بك

أفضل الممارسات هي إنشاء ملف FAT JAR واحد يحتوي على جميع ملفات .class المطلوبة لتشغيل التطبيق الخاص بك. يمكن نشر نفس ملف JAR في بيئة مختلفة من الاختبار إلى الإنتاج دون أي تغيير في مكتبات التبعية. لبدء بناء تطبيقنا كمثال على JAR fat ، نحتاج إلى تكوين مكون إضافي Maven يسمى maven-shade. يجب عليك إضافة الإدخالات التالية في قسم الملحقات في ملف pom.xml الخاص بك.

هنا نموذج تكوين Maven لبناء ملف 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>

تشغيل التطبيق الخاص بك

الآن ، يجب أن نكون قادرين على تشغيل الخدمة. إذا كنت قد أنشأت ملف 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 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.

ممتاز! 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.