إطار عمل Quarkus: كيف يتم تنفيذ العمارة النظيفة فيه

مرحبا هبر!



بينما نواصل استكشافنا لأطر عمل Java الجديدة واهتمامك بكتاب Spring Boot ، فإننا نبحث في إطار عمل Quarkus الجديد لجافا. يمكنك العثور على وصف مفصل لها هنا ، ونقترح اليوم قراءة ترجمة مقال بسيط يوضح مدى ملاءمة الالتزام بهندسة معمارية نظيفة باستخدام Quarkus .



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



كنقطة انطلاق ، أخذت مشروع Maven البسيط الذي يحتوي على 5 وحدات قياسية لإنشاء تطبيق CRUD REST باتباع مبادئ الهندسة النظيفة:



  • domain: كائنات المجال وواجهات بوابة لهذه الكائنات
  • app-api: واجهات التطبيق المقابلة للحالات العملية
  • app-impl: تنفيذ هذه الحالات عن طريق مجال الموضوع. يعتمد على app-apiو domain.
  • infra-persistence: ينفذ بوابات تسمح للمجال بالتفاعل مع واجهة برمجة تطبيقات قاعدة البيانات. يعتمد على domain.
  • infra-web: يفتح الحالات المدروسة للتفاعل مع العالم الخارجي باستخدام REST. يعتمد على app-api.


بالإضافة إلى ذلك ، سننشئ وحدة نمطية main-partitionستكون بمثابة أداة تطبيق قابلة للنشر.



عند التخطيط للعمل مع Quarkus ، فإن الخطوة الأولى هي إضافة مواصفات BOM إلى ملف POM الخاص بمشروعك. سيقوم BOM هذا بإدارة جميع إصدارات التبعيات التي تستخدمها. ستحتاج أيضًا إلى تكوين المكونات الإضافية القياسية للمشاريع المخضرمة في أداة إدارة البرنامج المساعد ، مثل البرنامج المساعد surefire . أثناء عملك مع Quarkus ، ستقوم أيضًا بتهيئة المكون الإضافي الذي يحمل نفس الاسم هنا. أخيرًا وليس آخرًا ، هنا تحتاج إلى تكوين المكون الإضافي للعمل مع كل من الوحدات (في <build> <plugins> ... </plugins> </build>) ، وبالتحديد المكون الإضافي Jandex... نظرًا لأن Quarkus يستخدم CDI ، فإن البرنامج المساعد Jandex يضيف ملف فهرس لكل وحدة ؛ يحتوي الملف على سجلات لجميع التعليقات التوضيحية المستخدمة في هذه الوحدة وروابط تشير إلى مكان استخدام التعليقات التوضيحية. نتيجة لذلك ، يكون التعامل مع CDI أسهل بكثير ، مع عمل أقل بكثير يتعين القيام به لاحقًا.



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



أولاً ، نقوم بتهيئة البنية لاستخدام المكون الإضافي Quarkus:



<build>
  <plugins>
    <plugin>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-maven-plugin</artifactId>
      <executions>
        <execution>
          <goals>
            <goal>build</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>


بعد ذلك ، دعنا نضيف التبعيات إلى كل من وحدات التطبيق ، حيث ستكون جنبًا إلى جنب مع التبعيات quarkus-resteasyو quarkus-jdbc-mysql. في التبعية الأخيرة ، يمكنك استبدال قاعدة البيانات بالقاعدة التي تفضلها (مع الأخذ في الاعتبار أننا سنتبع مسار التطوير الأصلي لاحقًا ، وبالتالي لا يمكننا استخدام قاعدة بيانات مضمنة ، على سبيل المثال ، H2).



بدلاً من ذلك ، يمكنك إضافة ملف تعريف حتى تتمكن من إنشاء التطبيق الأصلي لاحقًا. للقيام بذلك ، تحتاج حقًا إلى حامل تطوير إضافي (GraalVM native-imageو XCode إذا كنت تستخدم OSX).



<profiles>
  <profile>
    <id>native</id>
    <activation>
      <property>
        <name>native</name>
      </property>
    </activation>
    <properties>
      <quarkus.package.type>native</quarkus.package.type>
    </properties>
  </profile>
</profiles>


الآن ، إذا قمت mvn package quarkus:devبالتشغيل من جذر المشروع ، فلديك تطبيق Quarkus يعمل! ليس هناك الكثير لرؤيته حتى الآن ، حيث ليس لدينا أي عناصر تحكم أو محتوى حتى الآن.



إضافة وحدة تحكم REST



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



لاستخدام واجهة برمجة تطبيقات JAX-RS ، يجب إضافة تبعية إلى دليل معلومات شبكة الويب:



<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>


يبدو أبسط كود تحكم كما يلي:



@Path("/customer")
@Produces(MediaType.APPLICATION_JSON)
public class CustomerResource {
    @GET
    public List<JsonCustomer> list() {
        return getCustomers.getCustomer().stream()
                .map(response -> new JsonCustomer(response.getName()))
                .collect(Collectors.toList());
    }

    public static class JsonCustomer {
        private String name;

        public JsonCustomer(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }


إذا قمنا بتشغيل التطبيق الآن ، فيمكننا الاتصال بالمضيف المحلي : 8080 / عميل ورؤيته Joeبتنسيق JSON.



أضف حالة معينة



بعد ذلك ، دعنا نضيف حالة وتنفيذ لهذه الحالة العملية. دعنا app-apiنحدد الحالة التالية:



public interface GetCustomers {
    List<Response> getCustomers();

    class Response {
        private String name;

        public Response(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }
}


و app-implإنشاء تنفيذ بسيط من هذه الواجهة.



@UseCase
public class GetCustomersImpl implements GetCustomers {
    private CustomerGateway customerGateway;

    public GetCustomersImpl(CustomerGateway customerGateway) {
        this.customerGateway = customerGateway;
    }

    @Override
    public List<Response> getCustomers() {
        return Arrays.asList(new Response("Jim"));
    }
}


لكي ترى CDI المكون GetCustomersImpl، تحتاج إلى تعليق توضيحي مخصص UseCaseكما هو محدد أدناه. يمكنك أيضًا استخدام ApplicationScoped القياسي والتعليق التوضيحي Transactional، ولكن من خلال إنشاء التعليق التوضيحي الخاص بك ، يمكنك الحصول على القدرة على تجميع التعليمات البرمجية بشكل منطقي وفصل رمز التنفيذ الخاص بك عن أطر عمل مثل CDI.



@ApplicationScoped
@Transactional
@Stereotype
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
}


لاستخدام شروح CDI، يجب إضافة التبعيات التالية إلى ملف POM app-implبالإضافة إلى app-apiو التبعيات domain.



<dependency>
  <groupId>jakarta.enterprise</groupId>
  <artifactId>jakarta.enterprise.cdi-api</artifactId>
</dependency>
<dependency>
  <groupId>jakarta.transaction</groupId>
  <artifactId>jakarta.transaction-api</artifactId>
</dependency>


بعد ذلك ، نحتاج إلى تعديل وحدة التحكم REST لاستخدامها في الحالات app-api.



...
private GetCustomers getCustomers;

public CustomerResource(GetCustomers getCustomers) {
    this.getCustomers = getCustomers;
}

@GET
public List<JsonCustomer> list() {
    return getCustomers.getCustomer().stream()
            .map(response -> new JsonCustomer(response.getName()))
            .collect(Collectors.toList());
}
...


إذا قمت الآن بتشغيل التطبيق واستدعاء localhost : 8080 / customer ، فسترى ذلك Jimبتنسيق JSON.



تعريف وتنفيذ المجال



بعد ذلك ، سوف نركز على المجال. الجوهر هنا domainبسيط للغاية ، فهو يتكون من Customerواجهة بوابة نستقبل من خلالها المستهلكين.



public class Customer {
	private String name;

	public Customer(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}
}
public interface CustomerGateway {
	List<Customer> getAllCustomers();
}


نحتاج أيضًا إلى توفير تطبيق للبوابة قبل أن نتمكن من البدء في استخدامها. نحن نقدم هذه الواجهة بتنسيق infra-persistence.



بالنسبة لهذا التطبيق ، سنستخدم دعم JPA المتاح في Quarkus ، وكذلك استخدام إطار عمل Panache ، مما سيجعل حياتنا أسهل قليلاً. بالإضافة إلى المجال ، سيتعين علينا إضافة infra-persistenceالتبعية التالية إلى:



<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>


أولاً ، نحدد كيان JPA المقابل للمستهلك.



@Entity
public class CustomerJpa {
	@Id
	@GeneratedValue
	private Long id;
	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}


عند العمل مع Panache ، يمكنك اختيار أحد الخيارين: إما أن ترث الكيانات الخاصة بك PanacheEntity ، أو ستستخدم نموذج المستودع / DAO. لست من المعجبين بنمط ActiveRecord ، لذا سأتوقف عند المستودع بنفسي ، لكن ما ستعمل معه متروك لك.



@ApplicationScoped
public class CustomerRepository implements PanacheRepository<CustomerJpa> {
}


الآن وبعد أن أصبح لدينا كيان JPA ومستودعنا ، يمكننا تنفيذ البوابة Customer.



@ApplicationScoped
public class CustomerGatewayImpl implements CustomerGateway {
	private CustomerRepository customerRepository;

	@Inject
	public CustomerGatewayImpl(CustomerRepository customerRepository) {
		this.customerRepository = customerRepository;
	}

	@Override
	public List<Customer> getAllCustomers() {
		return customerRepository.findAll().stream()
				.map(c -> new Customer(c.getName()))
				.collect(Collectors.toList());
	}
}


الآن يمكنك تغيير الكود في تنفيذ حالتنا بحيث يستخدم البوابة.



...
private CustomerGateway customerGateway;

@Inject
public GetCustomersImpl(CustomerGateway customerGateway) {
    this.customerGateway = customerGateway;
}

@Override
public List<Response> getCustomer() {
    return customerGateway.getAllCustomers().stream()
            .map(customer -> new GetCustomers.Response(customer.getName()))
            .collect(Collectors.toList());
}
...


لا يمكننا بدء تطبيقنا حتى الآن ، لأن تطبيق Quarkus لا يزال بحاجة إلى التهيئة باستخدام معلمات الثبات المطلوبة. في src/main/resources/application.propertiesالوحدة النمطية ، main-partitionأضف المعلمات التالية.



quarkus.datasource.url=jdbc:mysql://localhost/test
quarkus.datasource.driver=com.mysql.cj.jdbc.Driver
quarkus.hibernate-orm.dialect=org.hibernate.dialect.MySQL8Dialect
quarkus.datasource.username=root
quarkus.datasource.password=root
quarkus.datasource.max-size=8
quarkus.datasource.min-size=2
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.sql-load-script=import.sql


لعرض البيانات الأصلية ، سنقوم أيضًا بإضافة الملف import.sqlإلى نفس الدليل الذي يتم إضافة البيانات منه.



insert into CustomerJpa(id, name) values(1, 'Joe');
insert into CustomerJpa(id, name) values(2, 'Jim');


إذا قمت الآن بتشغيل التطبيق واستدعاء localhost : 8080 / customer ، فسترى Joeذلك Jimبتنسيق JSON أيضًا. لذلك ، لدينا تطبيق كامل ، من REST إلى قاعدة البيانات.



الخيار الأصلي



إذا كنت تريد إنشاء تطبيق أصلي ، فأنت بحاجة إلى القيام بذلك باستخدام الأمر mvn package -Pnative. قد يستغرق هذا حوالي دقيقتين ، اعتمادًا على ماهية منصة التطوير الخاصة بك. Quarkus سريع جدًا عند بدء التشغيل وبدون دعم أصلي ، يبدأ في غضون 2-3 ثوانٍ ، ولكن عند تجميعه في ملف تنفيذي أصلي باستخدام GraalVM ، يتم تقليل الوقت المقابل إلى أقل من 100 مللي ثانية. بالنسبة لتطبيق Java ، فهذا سريع للغاية.



اختبارات



يمكنك اختبار تطبيق Quarkus باستخدام إطار اختبار Quarkus المقابل. إذا قمت بتعليق الاختبار @QuarkusTest، فسيقوم JUnit أولاً بتشغيل سياق Quarkus ثم تنفيذ الاختبار. main-partitionسيبدو اختبار التطبيق بالكامل كما يلي:



@QuarkusTest
public class CustomerResourceTest {
	@Test
	public void testList() {
		given()
				.when().get("/customer")
				.then()
				.statusCode(200)
				.body("$.size()", is(2),
						"name", containsInAnyOrder("Joe", "Jim"));
	}
}


خاتمة



من نواح كثيرة ، يعتبر Quarkus منافسًا شرسًا لـ Spring Boot. في رأيي ، بعض الأشياء في Quarkus تم حلها بشكل أفضل. على الرغم من أن app-impl لها تبعية إطار عمل ، إلا أنها مجرد تبعية للتعليقات التوضيحية (في حالة Spring ، عندما نضيف spring-contextللحصول على @Component، نضيف الكثير من تبعيات Spring الأساسية). إذا لم يعجبك هذا ، يمكنك أيضًا إضافة ملف Java إلى القسم الرئيسي ، باستخدام التعليق التوضيحي @Producesمن CDI وإنشاء المكون هناك ؛ في هذه الحالة ، لن تحتاج إلى أي تبعيات إضافية في app-impl. لكن لسبب ما ، jakarta.enterprise.cdi-apiأريد أن أرى الإدمان أقل من الإدمان spring-context.



Quarkus سريع وسريع حقًا. إنه أسرع من Spring Boot مع هذا النوع من التطبيقات. نظرًا لأنه وفقًا لـ Clean Architecture ، يجب أن تكون معظم تبعيات إطار العمل (إن لم يكن كلها) موجودة على السطح الخارجي للتطبيق ، يصبح الاختيار بين Quarkus و Spring Boot واضحًا. في هذا الصدد ، تتمثل ميزة Quarkus في أنه تم إنشاؤه على الفور مع مراعاة دعم GraalVM ، وبالتالي ، وبتكلفة أقل من الجهد ، فإنه يسمح لك بتحويل التطبيق إلى تطبيق أصلي. لا يزال Spring Boot متخلفًا عن Quarkus في هذا الصدد ، لكن ليس لدي أدنى شك في أنه سوف يلحق به قريبًا.



صحيح أن تجربة Quarkus ساعدتني أيضًا على إدراك العديد من المصائب التي تنتظر أولئك الذين يحاولون استخدام Quarkus مع خوادم تطبيقات Jakarta EE الكلاسيكية. في حين أنه لا يوجد الكثير الذي يمكن القيام به مع Quarkus حتى الآن ، فإن منشئ الكود الخاص به يدعم مجموعة متنوعة من التقنيات التي ليست سهلة الاستخدام بعد في سياق Jakarta EE مع خادم تطبيقات تقليدي. يغطي Quarkus جميع الأساسيات التي سيحتاجها الأشخاص المطلعون على Jakarta EE ، وتطويرها أكثر سلاسة. سيكون من المثير للاهتمام أن نرى كيف يمكن لنظام Java البيئي التعامل مع هذا النوع من المنافسة.



يتم نشر جميع الأكواد الخاصة بهذا المشروع على Github .



All Articles