เริ่มต้นใช้งานไมโครเซอร์วิส: บทช่วยสอน Dropwizard

เผยแพร่แล้ว: 2022-03-11

เราทุกคนต่างเห็นความนิยมเพิ่มขึ้นในสถาปัตยกรรมไมโครเซอร์วิส ในสถาปัตยกรรมไมโครเซอร์วิส Dropwizard สั่งงานที่สำคัญมาก เป็นเฟรมเวิร์กสำหรับสร้างบริการเว็บ RESTful หรือชุดเครื่องมือและเฟรมเวิร์กสำหรับสร้างบริการเว็บ RESTful ที่แม่นยำยิ่งขึ้น

ช่วยให้นักพัฒนาสามารถบูตสแตรปโครงการได้อย่างรวดเร็ว ซึ่งจะช่วยให้คุณจัดแพคเกจแอปพลิเคชันของคุณให้ปรับใช้ได้ง่ายในสภาพแวดล้อมการใช้งานจริงเป็นบริการแบบสแตนด์อโลน ตัวอย่างเช่น หากคุณเคยอยู่ในสถานการณ์ที่จำเป็นต้องบูตสแตรปโปรเจ็กต์ในเฟรมเวิร์ก Spring คุณอาจรู้ว่ามันเจ็บปวดเพียงใด

ภาพประกอบ: ตัวอย่างไมโครเซอร์วิสในบทช่วยสอน Dropwizard

ด้วย Dropwizard การเพิ่มการพึ่งพา Maven เพียงรายการเดียว

ในบล็อกนี้ ฉันจะแนะนำคุณตลอดขั้นตอนการเขียนบริการ Dropwizard RESTful อย่างง่าย หลังจากที่เราทำเสร็จแล้ว เราจะมีบริการสำหรับการดำเนินการ CRUD ขั้นพื้นฐานใน "ชิ้นส่วน" ไม่สำคัญหรอกว่า "ส่วน" คืออะไร; มันสามารถเป็นอะไรก็ได้ เพิ่งนึกขึ้นได้ก่อน

เราจะจัดเก็บข้อมูลในฐานข้อมูล MySQL โดยใช้ JDBI ในการสืบค้น และจะใช้ปลายทางต่อไปนี้:

  • GET /parts - เพื่อดึงข้อมูลทุกส่วนจาก DB
  • GET /part/{id} เพื่อรับส่วนใดส่วนหนึ่งจาก DB
  • POST /parts -เพื่อสร้าง part ใหม่
  • PUT /parts/{id} - เพื่อแก้ไขส่วนที่มีอยู่
  • DELETE /parts/{id} - เพื่อลบส่วนออกจาก DB

เราจะใช้ OAuth เพื่อตรวจสอบสิทธิ์บริการของเรา และสุดท้าย เพิ่มการทดสอบหน่วยลงไป

ไลบรารี Dropwizard เริ่มต้น

แทนที่จะรวมไลบรารีทั้งหมดที่จำเป็นในการสร้างบริการ REST แยกจากกันและกำหนดค่าแต่ละไลบรารี Dropwizard จะทำเพื่อเรา นี่คือรายการไลบรารีที่มาพร้อมกับ Dropwizard โดยค่าเริ่มต้น:

  • ท่าเทียบเรือ: คุณจะต้องใช้ HTTP เพื่อเรียกใช้เว็บแอปพลิเคชัน Dropwizard ฝังคอนเทนเนอร์เซิร์ฟเล็ต Jetty สำหรับการรันเว็บแอปพลิเคชัน แทนที่จะปรับใช้แอปพลิเคชันของคุณกับแอปพลิเคชันเซิร์ฟเวอร์หรือเว็บเซิร์ฟเวอร์ Dropwizard กำหนดวิธีการหลักที่เรียกใช้เซิร์ฟเวอร์ Jetty เป็นกระบวนการแบบสแตนด์อโลน ณ ตอนนี้ Dropwizard แนะนำให้เรียกใช้แอปพลิเคชันด้วย Jetty เท่านั้น บริการเว็บอื่น ๆ เช่น Tomcat ไม่ได้รับการสนับสนุนอย่างเป็นทางการ
  • Jersey: Jersey เป็นหนึ่งในการใช้งาน REST API ที่ดีที่สุดในตลาด นอกจากนี้ยังเป็นไปตามข้อกำหนด JAX-RS มาตรฐาน และเป็นการใช้งานอ้างอิงสำหรับข้อกำหนด JAX-RS Dropwizard ใช้ Jersey เป็นเฟรมเวิร์กเริ่มต้นสำหรับการสร้างเว็บแอปพลิเคชัน RESTful
  • Jackson: Jackson เป็นมาตรฐานโดยพฤตินัยสำหรับการจัดการรูปแบบ JSON เป็นหนึ่งใน API ตัวแมปวัตถุที่ดีที่สุดสำหรับรูปแบบ JSON
  • เมตริก: Dropwizard มีโมดูลเมตริกของตัวเองสำหรับเปิดเผยเมตริกของแอปพลิเคชันผ่านปลายทาง HTTP
  • ฝรั่ง: นอกเหนือจากโครงสร้างข้อมูลที่ไม่เปลี่ยนรูปที่ได้รับการปรับให้เหมาะสมที่สุดแล้ว Guava ยังมีคลาสจำนวนมากขึ้นเรื่อยๆ เพื่อเร่งการพัฒนาใน Java
  • Logback และ Slf4j: ทั้งสองใช้สำหรับกลไกการบันทึกที่ดีขึ้น
  • Freemarker and Mustache: การเลือกเทมเพลตเอ็นจิ้นสำหรับแอปพลิเคชันของคุณเป็นหนึ่งในการตัดสินใจที่สำคัญ เครื่องมือเทมเพลตที่เลือกจะต้องมีความยืดหยุ่นมากขึ้นสำหรับการเขียนสคริปต์ที่ดีขึ้น Dropwizard ใช้เครื่องมือเทมเพลตที่เป็นที่รู้จักและเป็นที่นิยมอย่าง Freemarker และ Mustache เพื่อสร้างอินเทอร์เฟซผู้ใช้

นอกเหนือจากรายการด้านบนแล้ว ยังมีไลบรารีอื่นๆ อีกมากมาย เช่น Joda Time, Liquibase, Apache HTTP Client และ Hibernate Validator ที่ Dropwizard ใช้สำหรับสร้างบริการ REST

การกำหนดค่า Maven

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

นี่เป็น 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<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 กับโมเดล ซึ่งเราทำโดยการลงทะเบียนคลาส mapper นี่คือคลาส mapper ของเรา:

 import java.sql.ResultSet; import java.sql.SQLException; import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.ResultSetMapper; import com.toptal.blog.model.Part; public class PartsMapper implements ResultSetMapper<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 ให้การสนับสนุนการตรวจสุขภาพแบบเนทีฟ ในกรณีของเรา เราอาจต้องการตรวจสอบว่าฐานข้อมูลทำงานอยู่หรือไม่ ก่อนที่จะแจ้งว่าบริการของเราดี สิ่งที่เราทำคือดำเนินการ 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 ที่แยกจากกัน ดังนั้นเราจึงจำเป็นต้องเพิ่มลงในการอ้างอิง:

 <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 ในกรณีของเรา Representation and Resource เราจะต้องเพิ่มการพึ่งพาต่อไปนี้ในไฟล์ 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 แบบอ้วน เราจำเป็นต้องกำหนดค่าปลั๊กอิน 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.