Βλέπουμε όλοι αύξηση της δημοτικότητας των αρχιτεκτονικών μικροεπηρεσιών. Σε μια αρχιτεκτονική microservice, το Dropwizard εντοπίζει ένα πολύ σημαντικό μέρος. Είναι ένα πλαίσιο για τη δημιουργία υπηρεσιών διαδικτύου RESTful ή, πιο συγκεκριμένα, ένα σύνολο εργαλείων και πλαισίων για τη δημιουργία υπηρεσιών διαδικτύου RESTful.
Επιτρέπει στους προγραμματιστές γρήγορη εκκίνηση έργου. Αυτό σας βοηθά να συσκευάσετε τις εφαρμογές σας για εύκολη ανάπτυξη σε περιβάλλον παραγωγής ως αυτόνομες υπηρεσίες. Εάν έχετε βρεθεί ποτέ σε μια κατάσταση όπου πρέπει να κάνετε bootstrap ένα έργο στο Άνοιξη πλαίσιο , για παράδειγμα, ίσως γνωρίζετε πόσο επώδυνο μπορεί να είναι.
Με το Dropwizard, είναι απλώς θέμα προσθήκης μιας εξάρτησης από το Maven.
Σε αυτό το blog, θα σας καθοδηγήσω στην πλήρη διαδικασία σύνταξης μιας απλής υπηρεσίας 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 με έναν πίνακα μόνο για χρήση στο παράδειγμά μας. Μπορείτε να βρείτε το init script για το σχήμα μέσα στην πηγή. Το 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 σε ένα μοντέλο, το οποίο κάνουμε με την εγγραφή μιας τάξης χαρτογράφησης. Εδώ είναι η τάξη χαρτών μας:
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 έχει δύο σημαντικές διεπαφές που πρέπει να εφαρμόσουμε.
Το πρώτο είναι το Authenticator. Η τάξη μας πρέπει να εφαρμόσει το 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
:
llc s corp c corp
{ '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. Ας πάμε για ένα διάλειμμα και πάρουμε ένα φλιτζάνι τσάι. Έχετε κάνει πολύ καλή δουλειά.
Μπορείτε να χρησιμοποιήσετε οποιονδήποτε πελάτη 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 προσφέρει εξαιρετικά γρήγορο bootstrap του έργου σας. Και αυτό είναι πιθανώς το μεγαλύτερο πλεονέκτημα της χρήσης του Dropwizard.
Επίσης, θα περιλαμβάνει όλες τις αιχμές βιβλιοθήκες / εργαλεία που θα χρειαστείτε ποτέ για την ανάπτυξη της υπηρεσίας σας. Οπότε σίγουρα δεν χρειάζεται να ανησυχείτε για αυτό. Σας δίνει επίσης πολύ ωραία διαχείριση διαμόρφωσης. Φυσικά, το Dropwizard έχει και κάποια μειονεκτήματα. Χρησιμοποιώντας το Dropwizard, είστε περιορισμένοι στη χρήση όσων προσφέρει ή υποστηρίζει το Dropwizard. Χάνετε μέρος της ελευθερίας που μπορεί να συνηθίσετε κατά την ανάπτυξη. Ωστόσο, δεν θα το έλεγα ούτε ως μειονέκτημα, καθώς αυτό ακριβώς κάνει το Dropwizard αυτό που είναι - εύκολο στη ρύθμιση, εύκολο στην ανάπτυξη, αλλά και ένα πολύ ισχυρό και υψηλής απόδοσης πλαίσιο REST.
Κατά τη γνώμη μου, η προσθήκη πολυπλοκότητας στο πλαίσιο υποστηρίζοντας όλο και περισσότερες βιβλιοθήκες τρίτων μερών θα εισήγαγε επίσης περιττή πολυπλοκότητα στην ανάπτυξη.
Το Representational State Transfer (REST) είναι ένα στυλ αρχιτεκτονικής (δεν πρέπει να συνδυάζεται με το σύνολο των προτύπων) που βασίζεται σε ένα σύνολο αρχών που περιγράφουν τον τρόπο καθορισμού και αντιμετώπισης των δικτυακών πόρων.
Ένα τελικό σημείο REST είναι ένα εκτεθειμένο σημείο εισόδου HTTP μιας υπηρεσίας REST. Στο παραπάνω παράδειγμα, το GET / parts είναι ένα τελικό σημείο ενώ το POST / parts είναι άλλο.
Μια υπηρεσία ιστού REST είναι οποιαδήποτε εφαρμογή ιστού που έχει δομηθεί σύμφωνα με το στυλ αρχιτεκτονικής REST. Ακούει τη θύρα HTTP ή HTTP για αιτήματα και εκθέτει τα τελικά της σημεία στον πελάτη.
Είναι μια αρχιτεκτονική χαλαρά συνδεδεμένων υπηρεσιών. Ωστόσο, αυτές οι υπηρεσίες εφαρμόζουν από κοινού την επιχειρηματική λογική ολόκληρου του συστήματος. Το μεγαλύτερο πλεονέκτημα των μικροϋπηρεσιών έγκειται στο γεγονός ότι κάθε μικρή υπηρεσία είναι πιο εύκολη στην ανάπτυξη, την κατανόηση και την ανάπτυξη ή συντήρηση χωρίς να επηρεάζεται το υπόλοιπο σύστημα.