portaldacalheta.pt
  • Κύριος
  • Επιστήμη Δεδομένων Και Βάσεις Δεδομένων
  • Κατανεμημένες Ομάδες
  • Ευκίνητο Ταλέντο
  • Κερδοφορία & Αποδοτικότητα
Επιστήμη Δεδομένων Και Βάσεις Δεδομένων

Ξεκινήστε με μικροσυσκευές: Ένα εκπαιδευτικό πρόγραμμα Dropwizard



Βλέπουμε όλοι αύξηση της δημοτικότητας των αρχιτεκτονικών μικροεπηρεσιών. Σε μια αρχιτεκτονική microservice, το Dropwizard εντοπίζει ένα πολύ σημαντικό μέρος. Είναι ένα πλαίσιο για τη δημιουργία υπηρεσιών διαδικτύου RESTful ή, πιο συγκεκριμένα, ένα σύνολο εργαλείων και πλαισίων για τη δημιουργία υπηρεσιών διαδικτύου RESTful.

Επιτρέπει στους προγραμματιστές γρήγορη εκκίνηση έργου. Αυτό σας βοηθά να συσκευάσετε τις εφαρμογές σας για εύκολη ανάπτυξη σε περιβάλλον παραγωγής ως αυτόνομες υπηρεσίες. Εάν έχετε βρεθεί ποτέ σε μια κατάσταση όπου πρέπει να κάνετε bootstrap ένα έργο στο Άνοιξη πλαίσιο , για παράδειγμα, ίσως γνωρίζετε πόσο επώδυνο μπορεί να είναι.



Εικόνα: Παράδειγμα μικροσυσκευών στο εκπαιδευτικό πρόγραμμα Dropwizard.



Με το Dropwizard, είναι απλώς θέμα προσθήκης μιας εξάρτησης από το Maven.



Σε αυτό το blog, θα σας καθοδηγήσω στην πλήρη διαδικασία σύνταξης μιας απλής υπηρεσίας Dropwizard RESTful. Αφού τελειώσουμε, θα έχουμε μια υπηρεσία για βασικές λειτουργίες CRUD σε 'ανταλλακτικά'. Δεν έχει σημασία τι είναι το 'μέρος'. μπορεί να είναι οτιδήποτε. Μόλις ήρθε στο μυαλό πρώτα.

Θα αποθηκεύσουμε τα δεδομένα σε μια βάση δεδομένων MySQL, χρησιμοποιώντας το JDBI για την υποβολή ερωτημάτων και θα χρησιμοποιήσουμε τα ακόλουθα τελικά σημεία:



  • GET /parts -για να ανακτήσετε όλα τα μέρη από το DB
  • GET /part/{id} για να λάβετε ένα συγκεκριμένο μέρος από το DB
  • POST /parts -για να δημιουργήσετε νέο μέρος
  • PUT /parts/{id} -για να επεξεργαστείτε ένα υπάρχον μέρος
  • DELETE /parts/{id} -για να διαγράψετε το τμήμα από ένα DB

Θα χρησιμοποιήσουμε το OAuth για έλεγχο ταυτότητας της υπηρεσίας μας και, τέλος, θα προσθέσουμε ορισμένες δοκιμές μονάδας σε αυτήν.

Προεπιλεγμένες βιβλιοθήκες Dropwizard

Αντί να συμπεριλάβει όλες τις βιβλιοθήκες που απαιτούνται για τη δημιουργία μιας υπηρεσίας REST ξεχωριστά και τη διαμόρφωση καθεμιάς από αυτές, το Dropwizard το κάνει αυτό για εμάς. Αυτή είναι η λίστα των βιβλιοθηκών που συνοδεύουν το Dropwizard από προεπιλογή:



  • Προβλήτα: Θα χρειαστείτε HTTP για την εκτέλεση μιας εφαρμογής ιστού. Το Dropwizard ενσωματώνει το κοντέινερ Jetty για την εκτέλεση εφαρμογών ιστού. Αντί να αναπτύξετε τις εφαρμογές σας σε διακομιστή εφαρμογών ή διακομιστή ιστού, το Dropwizard καθορίζει μια κύρια μέθοδο που καλεί τον διακομιστή Jetty ως αυτόνομη διαδικασία. Από τώρα, το Dropwizard συνιστά την εκτέλεση της εφαρμογής μόνο με το Jetty. άλλες υπηρεσίες Ιστού όπως το Tomcat δεν υποστηρίζονται επίσημα.
  • Φανέλα: Το Jersey είναι μια από τις καλύτερες εφαρμογές REST API στην αγορά. Επίσης, ακολουθεί την τυπική προδιαγραφή JAX-RS και είναι η εφαρμογή αναφοράς για τις προδιαγραφές JAX-RS. Το Dropwizard χρησιμοποιεί το Jersey ως το προεπιλεγμένο πλαίσιο για τη δημιουργία εφαρμογών ιστού RESTful.
  • Τζάκσον: Το Jackson είναι το de facto πρότυπο για το χειρισμό μορφής JSON. Είναι ένα από τα καλύτερα API mapper αντικειμένων για τη μορφή JSON.
  • Μετρήσεις: Το Dropwizard έχει τη δική του μονάδα μετρήσεων για την έκθεση των μετρήσεων της εφαρμογής μέσω των τελικών σημείων HTTP.
  • Γκουάβα: Εκτός από τις εξαιρετικά βελτιστοποιημένες αμετάβλητες δομές δεδομένων, το Guava παρέχει έναν αυξανόμενο αριθμό τάξεων για επιτάχυνση ανάπτυξη στην Ιάβα .
  • Αποσύνδεση και Slf4j: Αυτά τα δύο χρησιμοποιούνται για καλύτερους μηχανισμούς καταγραφής.
  • Freemarker και μουστάκι: Η επιλογή μηχανών προτύπου για την εφαρμογή σας είναι μία από τις βασικές αποφάσεις. Η επιλεγμένη μηχανή προτύπου πρέπει να είναι πιο ευέλικτη για τη σύνταξη καλύτερων σεναρίων. Το Dropwizard χρησιμοποιεί γνωστούς και δημοφιλείς κινητήρες προτύπων Freemarker και Mustache για τη δημιουργία διεπαφών χρήστη.

Εκτός από την παραπάνω λίστα, υπάρχουν πολλές άλλες βιβλιοθήκες όπως Joda Time, Liquibase, Apache HTTP Client και Hibernate Validator που χρησιμοποιούνται από το Dropwizard για τη δημιουργία υπηρεσιών REST.

Διαμόρφωση Maven

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

Το τελευταίο μέρος του είναι στην πραγματικότητα μια εφαρμογή ελέγχου υγείας, για την οποία θα μιλήσουμε αργότερα.

Επίπεδο DAO, JDBI και Mapper

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

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

Ας προσθέσουμε μερικές δοκιμές μονάδας στην εφαρμογή μας. Θα επιμείνω στη δοκιμή συγκεκριμένων τμημάτων του κώδικα του 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 { } }

Δημιουργήστε την εφαρμογή Dropwizard

Η βέλτιστη πρακτική είναι να δημιουργήσετε το μεμονωμένο αρχείο 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

Τώρα, όταν έχετε ενεργοποιήσει την υπηρεσία Dropwizard REST, ας συνοψίσουμε ορισμένα βασικά πλεονεκτήματα ή μειονεκτήματα της χρήσης του Dropwizard ως πλαισίου REST. Είναι απολύτως προφανές από αυτήν την ανάρτηση ότι το Dropwizard προσφέρει εξαιρετικά γρήγορο bootstrap του έργου σας. Και αυτό είναι πιθανώς το μεγαλύτερο πλεονέκτημα της χρήσης του Dropwizard.

Επίσης, θα περιλαμβάνει όλες τις αιχμές βιβλιοθήκες / εργαλεία που θα χρειαστείτε ποτέ για την ανάπτυξη της υπηρεσίας σας. Οπότε σίγουρα δεν χρειάζεται να ανησυχείτε για αυτό. Σας δίνει επίσης πολύ ωραία διαχείριση διαμόρφωσης. Φυσικά, το Dropwizard έχει και κάποια μειονεκτήματα. Χρησιμοποιώντας το Dropwizard, είστε περιορισμένοι στη χρήση όσων προσφέρει ή υποστηρίζει το Dropwizard. Χάνετε μέρος της ελευθερίας που μπορεί να συνηθίσετε κατά την ανάπτυξη. Ωστόσο, δεν θα το έλεγα ούτε ως μειονέκτημα, καθώς αυτό ακριβώς κάνει το Dropwizard αυτό που είναι - εύκολο στη ρύθμιση, εύκολο στην ανάπτυξη, αλλά και ένα πολύ ισχυρό και υψηλής απόδοσης πλαίσιο REST.

Κατά τη γνώμη μου, η προσθήκη πολυπλοκότητας στο πλαίσιο υποστηρίζοντας όλο και περισσότερες βιβλιοθήκες τρίτων μερών θα εισήγαγε επίσης περιττή πολυπλοκότητα στην ανάπτυξη.

Κατανόηση των βασικών

Ποια είναι η έννοια της αντιπροσωπευτικής μεταφοράς κατάστασης (REST);

Το Representational State Transfer (REST) ​​είναι ένα στυλ αρχιτεκτονικής (δεν πρέπει να συνδυάζεται με το σύνολο των προτύπων) που βασίζεται σε ένα σύνολο αρχών που περιγράφουν τον τρόπο καθορισμού και αντιμετώπισης των δικτυακών πόρων.

Τι είναι το τελικό σημείο REST;

Ένα τελικό σημείο REST είναι ένα εκτεθειμένο σημείο εισόδου HTTP μιας υπηρεσίας REST. Στο παραπάνω παράδειγμα, το GET / parts είναι ένα τελικό σημείο ενώ το POST / parts είναι άλλο.

Τι είναι η διαδικτυακή υπηρεσία REST;

Μια υπηρεσία ιστού REST είναι οποιαδήποτε εφαρμογή ιστού που έχει δομηθεί σύμφωνα με το στυλ αρχιτεκτονικής REST. Ακούει τη θύρα HTTP ή HTTP για αιτήματα και εκθέτει τα τελικά της σημεία στον πελάτη.

Τι είναι η αρχιτεκτονική μικροϋπηρεσιών;

Είναι μια αρχιτεκτονική χαλαρά συνδεδεμένων υπηρεσιών. Ωστόσο, αυτές οι υπηρεσίες εφαρμόζουν από κοινού την επιχειρηματική λογική ολόκληρου του συστήματος. Το μεγαλύτερο πλεονέκτημα των μικροϋπηρεσιών έγκειται στο γεγονός ότι κάθε μικρή υπηρεσία είναι πιο εύκολη στην ανάπτυξη, την κατανόηση και την ανάπτυξη ή συντήρηση χωρίς να επηρεάζεται το υπόλοιπο σύστημα.

Δημιουργία εφαρμογών πολλαπλών πλατφορμών με το Xamarin: Προοπτική προγραμματιστή Android

Κινητό

Δημιουργία εφαρμογών πολλαπλών πλατφορμών με το Xamarin: Προοπτική προγραμματιστή Android
Μάθετε αυτές τις δημοφιλείς τάσεις με αυτά τα μαθήματα του Photoshop

Μάθετε αυτές τις δημοφιλείς τάσεις με αυτά τα μαθήματα του Photoshop

Εργαλεία Και Σεμινάρια

Δημοφιλείς Αναρτήσεις
Πώς να επιλέξετε το καλύτερο πλαίσιο Front-End
Πώς να επιλέξετε το καλύτερο πλαίσιο Front-End
Χρειάζεστε έναν ήρωα: Ο υπεύθυνος έργου
Χρειάζεστε έναν ήρωα: Ο υπεύθυνος έργου
Πώς να βελτιώσετε την απόδοση της εφαρμογής ASP.NET στο Web Farm με προσωρινή αποθήκευση
Πώς να βελτιώσετε την απόδοση της εφαρμογής ASP.NET στο Web Farm με προσωρινή αποθήκευση
Οι δοκιμασμένοι και αληθινοί νόμοι του UX (με Infographic)
Οι δοκιμασμένοι και αληθινοί νόμοι του UX (με Infographic)
Ανώτερος συνεργάτης πελάτη, υγειονομική περίθαλψη και βιοεπιστήμες
Ανώτερος συνεργάτης πελάτη, υγειονομική περίθαλψη και βιοεπιστήμες
 
Η άνοδος των αυτοματοποιημένων συναλλαγών: Μηχανές που εμπορεύονται το S&P 500
Η άνοδος των αυτοματοποιημένων συναλλαγών: Μηχανές που εμπορεύονται το S&P 500
10 πιο κοινές ευπάθειες ασφαλείας στον Ιστό
10 πιο κοινές ευπάθειες ασφαλείας στον Ιστό
Σκέψεις για τη συγκέντρωση του ιδιωτικού σας αμοιβαίου κεφαλαίου
Σκέψεις για τη συγκέντρωση του ιδιωτικού σας αμοιβαίου κεφαλαίου
Διευθυντής έργου και διαχείρισης προϊόντων
Διευθυντής έργου και διαχείρισης προϊόντων
Η σημασία της διατήρησης πελατών - μια εμπειρική μελέτη
Η σημασία της διατήρησης πελατών - μια εμπειρική μελέτη
Δημοφιλείς Αναρτήσεις
  • Οδηγός powerpivot excel 2013 pdf
  • μοντέλο αντικειμένου σελίδας σε σελήνιο
  • τι κάνουν τα bots σε διαφωνία
  • angularjs 2 βήμα προς βήμα
  • τα οποία χρωματικά σχέδια είναι καταπραϋντικά για το μάτι αλλά στερούνται οπτικής ζωντάνιας και αντίθεσης
  • Παράδειγμα παρτίδας ελατηρίου βήμα προς βήμα
Κατηγορίες
  • Επιστήμη Δεδομένων Και Βάσεις Δεδομένων
  • Κατανεμημένες Ομάδες
  • Ευκίνητο Ταλέντο
  • Κερδοφορία & Αποδοτικότητα
  • © 2022 | Ολα Τα Δικαιώματα Διατηρούνται

    portaldacalheta.pt