范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

使用DDD和SpringHATEOAS构建一个MRP的API实例和源码elca

  通过一个具体的例子告诉你我们如何在 Java 中实现一个只允许根据业务规则定义良好的状态转换的域模型,然后使用 Spring 在一个REST-API 中发布它。看看我们如何构建一个完全由该 API 驱动的简单 Web 应用程序。该实现使用来自领域驱动设计(DDD) 的概念,这是一种软件工件试图与业务模型一致的方法,例如通过使用领域语言的术语。
  CRUD 应用程序的问题
  create-read-update-delete 或简短的 CRUD 方法非常容易实现,它是有状态 Web 应用程序中将图形用户界面 (GUI) 与后端集成的主要方法。因此,后端的实体仅对需要持久化的领域逻辑的状态进行建模,而不是对适用于产生有效状态更改的业务操作和业务规则进行建模。
  在最好的情况下,服务操作会为这些操作命名并包含实现所需业务规则的逻辑。这些规则很容易从整个代码库中泄漏出来,并且也经常出现在图形用户界面中,现在通常实现为在浏览器中运行的单页 Web 应用程序。
  结果,我们有一个应用程序,它带有 Martin Fowler 所说的 贫血域模型 ,以及一个难以理解和维护的相互纠缠的泥球,因为职责没有明确分离。基于启发式,GUI 对实体可以做什么做出假设,并实现与后端分离的导航逻辑。
  HATEOAS 和 Richardson 成熟度模型
  查阅文献以寻找解决此问题的解决方案,您可能会遇到 HTTP 应用程序 API 的 Richardson 成熟度模型 。它从普通的旧 XML 开始,这意味着 XML 内容被发布到 Web 服务端点。 在 1级API中 引入了资源的概念,允许单独操作后端实体,从而将大型服务端点分解为多个资源。 2级API进一步使用特定的 HTTP 动词,如PUT、DELETE或PATCH来细化操作的含义。Martin Fowler 称它提供了一组标准的动词,以便我们以相同的方式处理类似的情况,消除不必要的变化。 在第 3级,通过向每个响应添加特定于上下文的超链接,将可发现性融入 API,让客户了解接下来可以使用给定资源执行哪些操作,或链接相关资源。在这个级别,与 API 交换的"超文本"充当应用程序状态的引擎,植根于后端并通过超链接向客户端(在我们的例子中为 Web 前端)显示。
  REST-ful API 和 HAL 标准如何帮助我们解决将业务逻辑封装在后端的初始问题?让我们看一个具体的例子。
  示例领域:制造资源计划
  假设我们正在构建一个制造资源计划系统( MRP ),产品经理可以在其中准备和提交生产订单。然后制造商可以接受订单,指明预计的交货日期,并在产品生产后完成订单。
  此外,以下业务规则适用: 产品经理在提交后无法更改生产订单。 当制造商接受生产订单时,他必须指明未来可以完成订单的日期。
  使用 Spring 和 Angular 设置项目
  正如 Josh Long 一直告诉我们的,每个项目都应该从 https://start.spring.io 开始。事实上,该页面非常方便,让我们可以轻松地引导一个包含所需技术的新应用程序。对于我们的案例,我们选择以下依赖项: Spring Data JDBC:基于 java 数据库连接 (JDBC) 的直接 OR 映射器,使我们免于 JPA 的开销,非常适合持久化  DDD  风格的聚合 H2 数据库:用 Java 编写的关系数据库,可以开箱即用地在内存中运行 Rest Repositories:一个 Spring 库,允许将我们的聚合发布为 REST 资源 Lombok:一个字节码生成器,它极大地减少了样板代码的数量,并为 Java 提供了一些现代语言,如 Kotlin 或 TypeScript Spring Boot DevTools:一个开发依赖,它会在代码库的每次更改时自动重新启动应用程序
  聚合:状态符合业务规则的地方
  域驱动应用程序的核心是域模型。它不受技术和集成方面的影响,并尽可能地遵循商业模式和术语。因此,应用程序的状态在所谓的实体中被捕获,其中相关的实体可以被分组以形成一个聚合体。每个聚合都定义了应用程序内部的一致性边界,这意味着只有明确定义的状态更改才会在聚合内以事务方式发生。
  为了保持专注,让我们从一个非常简单的生产订单模型开始,没有子实体,只有四个字段: id:区分不同生产订单的标识符。为简单起见,我们将其建模为 aLong并让数据库对其进行初始化。注释告诉 Spring Data JDBC这@Id是主键。 name:生产订单的名称。提交后,名称不得再更改。 expectedCompletionDate:制造商在接受生产订单时提供的日期,表明制造过程的计划完成 state:根据领域模型的生产订单状态。它可以假定值DRAFT, SUBMITTED, ACCEPTED, COMPLETED, 建模为枚举。
  我们使用 Lombok 的注解对类进行@Getter注解,它生成字节码以使用 getter 来检测我们的(普通)聚合,以便从外部访问这些字段的值。
  我们现在如何确保域模型只允许定义良好的状态转换,而不是通过 setter 展示所有字段?答案是:通过执行各自的业务操作。
  当然,我们可以使用构造函数来创建我们的实体。然而,我更喜欢提供一个静态工厂方法,它允许我们使用适当的业务术语作为名称,而不是相当技术性的new语句。creat- 方法将产品订单的名称作为单个参数并将状态初始化为DRAFT。当聚合被持久化到数据库时,该id字段稍后将由框架初始化。
  初始的create方法: package com.example.demo.productionorders;  import java.time.LocalDate;  import org.springframework.data.annotation.Id;  import lombok.Getter; import lombok.val;  @Getter public class ProductionOrder {      @Id     private Long id;     private String name;     private LocalDate expectedCompletionDate;     private ProductionOrderState state;      public static ProductionOrder create(String name) {         val result = new ProductionOrder();         result.name = name;         result.state = ProductionOrderState.DRAFT;         return result;     }      public enum ProductionOrderState { DRAFT, SUBMITTED, ACCEPTED; }  }
  Repository库和基本 REST API
  为了将我们的聚合持久化到数据库并从那里检索它,我们定义了一个接口,为简单起见扩展了CrudRepositorySpring Data 的接口。我们没有在其名称中使用"存储库",而是将其命名ProductionOrders为持久保存我们领域无处不在的语言。
  为了将我们的聚合持久化到数据库并从那里检索它,我们定义了一个接口,为简单起见扩展了CrudRepositorySpring Data 的接口。我们没有在其名称中使用"存储库",而是将其命名ProductionOrders为坚持我们领域无处不在的语言。package com.example.demo.productionorders;  import org.springframework.data.repository.CrudRepository; import org.springframework.data.rest.core.annotation.RepositoryRestResource;  @RepositoryRestResource public interface ProductionOrders extends CrudRepository {  }
  启动应用程序并通过 curl 命令行工具查询其 API,我们得到以下响应:$ curl http://localhost:8080/api  {      "_links" : {      "productionOrders" : {        "href" : "http://localhost:8080/api/productionOrders"      },      "profile" : {        "href" : "http://localhost:8080/api/profile"      }    }  } 
  你注意到字段_links了吗?是的,Spring Data REST默认产生HAL格式的响应。它向我们表明,API提供了一个集合资源 "productionOrders",包括一个如何导航的链接。如果每个资源都提供了客户需要的所有链接,以导航到相关的资源和调用动作,那么我们就可以得出以下结论。
  客户端只需要知道一个URL,那就是"/api"。在一个真正的REST-ful API中,所有其他的URL都可以从API的响应中检索出来。
  为了进一步说明这个概念,我们在DemoApplication类中创建并持久化一些生产订单,然后关注productionOrders资源的href-Property。$ curl http://localhost:8080/api/productionOrders {   "_embedded" : {     "productionOrders" : [ {       "name" : "Order 1",       "expectedCompletionDate" : null,       "state" : "DRAFT",       "_links" : {         "self" : {           "href" : "http://localhost:8080/api/productionOrders/1"         },         "productionOrder" : {           "href" : "http://localhost:8080/api/productionOrders/1"         }       }     }, {       "name" : "Order 2",       "expectedCompletionDate" : null,       "state" : "DRAFT",       "_links" : {         "self" : {           "href" : "http://localhost:8080/api/productionOrders/2"         },         "productionOrder" : {           "href" : "http://localhost:8080/api/productionOrders/2"         }       }     } ]   },   "_links" : {     "self" : {       "href" : "http://localhost:8080/api/productionOrders"     },     "profile" : {       "href" : "http://localhost:8080/api/profile/productionOrders"     }   } } 
  我们看到,两个生产订单被返回,包含在HAL规范定义的特殊字段_embedded的一个属性中。如果与一个资源的交互需要额外的信息,例如下拉列表的值,以过滤特定状态下的生产订单,这些数据可以被添加到响应的_embedded属性下的另一个字段。
  每个生产订单资源都提供一组链接,默认情况下是相当琐碎的:一个自我链接和一个生产订单链接,都指向资源本身。作为下一步,我们现在将把业务操作添加到生产订单类中,并把调用它们的端点作为附加链接公开。
  增加业务行为
  如果我们回顾一下业务需求的大纲,我们的聚合体需要支持以下操作。当处于DRAFT状态时允许重命名一个向制造商提交订单的操作一个接受订单的操作,提供预期交货日期
  该实现遵循我们在创建方法中已经使用的方法。我们没有提供getters和setters,而是用符合我们领域语言的名字来实现方法:renameTo、submit、accept。
  如上所述,聚合被看作是一致性的边界。由于额外的方法不再是静态的,我们可以直接利用类的字段来执行所需的业务规则,并允许只执行定义明确的状态转换。例如,我们可以完全确定永远不会遇到没有完成日期的已接受的生产订单,这是我们第二个业务规则的要求。与基于setter的方法相比,这是一个多么大的区别啊!
  在生产订单总量中实施三种业务操作,只允许有明确的状态转换。package com.example.demo.productionorders;  import java.time.LocalDate; import java.util.Objects;  import org.springframework.data.annotation.Id;  import lombok.Getter; import lombok.val;  @Getter public class ProductionOrder {      @Id     private Long id;     private String name;     private LocalDate expectedCompletionDate;     private ProductionOrderState state;      public static ProductionOrder create(String name) {         val result = new ProductionOrder();         result.name = name;         result.state = ProductionOrderState.DRAFT;         return result;     }          public ProductionOrder renameTo(String newName) {         if (state != ProductionOrderState.DRAFT) {             throw new IllegalStateException("Cannot rename production order in state " + state);         }         name = newName;         return this;     }              public ProductionOrder submit() {         if (state != ProductionOrderState.DRAFT) {             throw new IllegalStateException("Cannot submit production order in state " + state);         }         state = ProductionOrderState.SUBMITTED;         return this;     }      public ProductionOrder accept(LocalDate expectedCompletionDate) {         if (state != ProductionOrderState.SUBMITTED) {             throw new IllegalStateException("Cannot accept production order in state " + state);         }         Objects.requireNonNull(expectedCompletionDate, "expectedCompletionDate is required to submit a production order");         if (expectedCompletionDate.isBefore(LocalDate.now())) {             throw new IllegalArgumentException("Expected completion date must be in the future, but was " + expectedCompletionDate);         }         state = ProductionOrderState.ACCEPTED;         this.expectedCompletionDate = expectedCompletionDate;         return this;     }      public enum ProductionOrderState { DRAFT, SUBMITTED, ACCEPTED; }  }   
  在REST API中公开业务能力
  作为下一步,我们现在要在REST API中公开业务操作。我们需要这样做的成分是。为每个行动提供新的端点,其形式为/api/productionOrders/{id}/{action}。ProductionOrder资源的HAL表示中的链接
  仔细想想,如果我们只暴露一个指向端点的链接,如果相应的动作实际上是允许的,这不是很好吗,这取决于特定生产订单的状态?这可以通过以下方式轻松实现。
  我们首先实现一个ProductionOrderController类,并在类的层面上将其映射到/api/productionOrders端点(如果你在映射中遇到麻烦,请参见bug https://github.com/spring-projects/spring-data-rest/issues/1342)。
  这使得我们可以通过额外的方法来扩展Spring Data REST提供的标准API:rename, submit, accept。这些方法从路径中获取生产订单的ID,并从请求体中获取任何额外的必要参数。由于与网络技术的集成是应用层的问题,我们把它放在子包web中,以便将其与领域逻辑明确分开。
  在聚合上应用动作的模式总是相同的:从持久性存储中加载聚合,调用业务操作,并将其保存到存储中。这同样适用于我们案例中的关系型持久化,但也适用于事件源模型。为了简单起见,我们在控制器中做了所有的事情,而在一个更大的或更纯粹的应用中,控制器将委托给一个域服务。
  关于这篇博文的主题,更有趣的部分是通过实现Spring HATEOAS的RepresentationModelProcessor接口。在这个方法过程中,它把生产订单包装成一个实体模型。这个实体模型允许向生产订单资源添加额外的链接。因为该模型也提供了生产订单本身,我们可以很容易地检查它的状态,然后决定是否生成一个特定的链接。
  Spring提供了静态的辅助方法linkTo和methodOn来动态地导出引用的控制器方法的URL。package com.example.demo.productionorders.web;  import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;  import java.time.LocalDate;  import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.server.RepresentationModelProcessor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException;  import com.example.demo.productionorders.ProductionOrder; import com.example.demo.productionorders.ProductionOrder.ProductionOrderState; import com.example.demo.productionorders.ProductionOrders;  import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Value; import lombok.val;  @RestController @RequestMapping("/api/productionOrders") @RequiredArgsConstructor public class ProductionOrderController implements RepresentationModelProcessor> {      public static final String REL_RENAME = "rename";     public static final String REL_SUBMIT = "submit";     public static final String REL_ACCEPT = "accept";          private final ProductionOrders productionOrders;          @PostMapping("/{id}/rename")     public ResponseEntity<?> rename(@PathVariable Long id, @RequestBody RenameRequest request) {         return productionOrders.findById(id)             .map(po -> productionOrders.save(po.renameTo(request.newName)))             .map(po -> ResponseEntity.ok().body(EntityModel.of(po)))             .orElse(ResponseEntity.notFound().build());     }      @PostMapping("/{id}/submit")     public ResponseEntity<?> submit(@PathVariable Long id) {         return productionOrders.findById(id)             .map(po -> productionOrders.save(po.submit()))             .map(po -> ResponseEntity.ok().body(EntityModel.of(po)))             .orElse(ResponseEntity.notFound().build());     }          @PostMapping("/{id}/accept")     public ResponseEntity<?> accept(@PathVariable Long id, @RequestBody CompleteRequest request) {         return productionOrders.findById(id)             .map(po -> productionOrders.save(po.accept(request.expectedCompletionDate)))             .map(po -> ResponseEntity.ok().body(EntityModel.of(po)))             .orElse(ResponseEntity.notFound().build());     }          @Override     public EntityModel process(EntityModel model) {                 val order = model.getContent();         if (order.getState() == ProductionOrderState.DRAFT) {             model.add(linkTo(methodOn(getClass()).rename(order.getId(), null)).withRel(REL_RENAME));             model.add(linkTo(methodOn(getClass()).submit(order.getId())).withRel(REL_SUBMIT));         }         if (order.getState() == ProductionOrderState.SUBMITTED) {             model.add(linkTo(methodOn(getClass()).accept(order.getId(), null)).withRel(REL_ACCEPT));         }                         return model;     }          @ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class})     void handleValidationException(Exception exception) {         throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, exception.getMessage());     }          @Value     static class RenameRequest {         @NonNull String newName;     }      @Value     static class CompleteRequest {         @NonNull LocalDate expectedCompletionDate;     }      } 
  再次查询productionOrders资源为我们提供了每个生产订单资源上的新链接。$ curl http://localhost:8080/api/productionOrders {   "_embedded" : {     "productionOrders" : [ {       "name" : "Order 1",       "expectedCompletionDate" : null,       "state" : "DRAFT",       "_links" : {         "self" : {           "href" : "http://localhost:8080/api/productionOrders/1"         },         "productionOrder" : {           "href" : "http://localhost:8080/api/productionOrders/1"         },         "rename" : {           "href" : "http://localhost:8080/api/productionOrder/1/rename"         },         "submit" : {           "href" : "http://localhost:8080/api/productionOrder/1/submit"         }       }     }, {       "name" : "Order 2",       "expectedCompletionDate" : null,       "state" : "DRAFT",       "_links" : {         "self" : {           "href" : "http://localhost:8080/api/productionOrders/2"         },         "productionOrder" : {           "href" : "http://localhost:8080/api/productionOrders/2"         },         "rename" : {           "href" : "http://localhost:8080/api/productionOrder/2/rename"         },         "submit" : {           "href" : "http://localhost:8080/api/productionOrder/2/submit"         }       }     } ]   },   "_links" : {     "self" : {       "href" : "http://localhost:8080/api/productionOrders"     },     "profile" : {       "href" : "http://localhost:8080/api/profile/productionOrders"     }   } } 
  你看到Spring并没有公开聚合的ID属性。我们稍后会看到,我们不需要在客户端知道它,因为它包含在链接中。
  还请注意,每个链接都有一个关系属性,简称 "rel"。这个属性非常重要,因为它定义了与API客户端的契约,即一个特定资源存在哪些链接。我们很快就会看到;我们的后端现在已经完成了,我们可以继续在前端利用它了。
  在前端消费HAL模型
  正如我之前所说,一个真正的REST-ful API的客户端应该只知道一个URL。/api。客户端调用的任何其他URL都应该从响应中的链接中获得。
  从我们Angular应用程序的顶级组件(即AppComponent)中发出对基本URL的请求,并将其作为输入传递给子组件,这将是一个自然的选择。为了保持简单,我们在ProductionOrderListComponent中做了所有事情,作为onInit方法的一部分获取API,并将响应存储在字段根中。见下面代码
  为了显示生产订单,我们在类顶部的@Component-decorator中添加一个HTML模板,并通过跟踪根资源中与 "productionOrders "有关的链接,从后台加载productionOrders。正如我们在上面看到的,这个链接的url属性是http://localhost:8080/api/productionOrders,但前端对此是不知道的。事实上,后端可以在一个完全不同的URL下提供生产订单,而我们的前端仍然可以工作。只有 "productionOrders "这个关系,也就是后端和前端之间的契约,必须保持稳定。
  最初的生产订单列表组件,首先加载API资源,然后通过各自关系下提供的URL加载生产订单。import { HttpClient } from "@angular/common/http"; import { Component, OnInit } from "@angular/core"; import { ProductionOrderResource } from "../model";  const API = "/api"; const REL = "productionOrders";   @Component({   selector: "app-production-order-list",   template: `   
  • "let order of productionOrders">{{order.name}} "order.expectedCompletionDate"> Expected for {{order.expectedCompletionDate|date}} ({{order.state}})
`, styleUrls: ["./production-order-list.component.css"] }) export class ProductionOrderListComponent implements OnInit { root: any; productionOrders?: ProductionOrderResource[]; constructor(private http: HttpClient) { } ngOnInit(): void { this.http.get(API).subscribe( response => { this.root = response; this.reload(); }, error => alert(error) ); } private reload(): void { if (this.root) { this.http.get(this.root._links[REL].href).subscribe( response => this.productionOrders = response._embedded[REL], error => alert(error) ) } } }   接下来,我们需要添加一种方式,让用户可以在模型上执行相应的业务操作。最简单的方法是为当前允许的每个动作在生产订单旁边添加一个按钮。   为每个生产订单添加按钮,根据生产订单资源中相关链接关系的存在与否,显示或隐藏这些按钮。
  • "let order of productionOrders">{{order.name}} "order.expectedCompletionDate"> Expected for {{order.expectedCompletionDate|date}} ({{order.state}})
  为了决定一个给定的动作是否被允许,以及相应的按钮是否应该被显示,我们查询底层资源,以获取与给定关系的链接。事实上,我们只是实现了一个功能切换:如果该关系被提供为一个链接,则该动作被启用,否则它在GUI中被隐藏。   请注意,示例代码(你可以在这篇文章的末尾找到链接)添加了一些接口,以允许对生产订单资源的_links-property进行类型安全的访问。   最后,我们只需要为那些需要提交额外数据的动作添加特殊处理。在这里,我们再次使用最简单的解决方案,使用本地的提示控制,在重命名动作的情况下接受新的名称,在接受动作的情况下接受预期完成日期。   一个非常简单的 "能 "和 "做 "方法的实现,利用关系和联系或生产秩序资源。can(action: string, order: any): boolean { return !!order._links[action]; } do(action: string, order: ProductionOrderResource): void { var body = {}; if (action === "rename") { const newName = prompt("Please enter the new name:"); if (!newName) { return; } body = { newName: newName }; } else if (action === "accept") { const expectedCompletionDate = prompt("Expected completion date (yyyy-MM-dd):"); if (!expectedCompletionDate) { return; } body = { expectedCompletionDate: expectedCompletionDate } } const url = order._links[action].href; this.http.post(url, body).subscribe( _ => this.reload(), response => alert([response.error.error, response.error.message].join(" "))); }   因为我们在can-method中检查了一个链接的存在,所以我们可以通过提取该链接的href-property轻松地确定要发布正文的URL。这样一来,我们的小演示程序的前端也就完成了。   总结   尽管许多开发者知道REST-ful APIs的Richardson成熟度模型,但只有少数人使用嵌入式链接来驱动应用状态。基于Open API(以前称为Swagger)的API文档将重点放在绝对URL上,而不是关系上。因此,前端代码经常被固定的URL所束缚,并复制了许多最好在后端进行的逻辑。   Spring HATEOAS为利用REST的全部潜力提供了所有必要的工具。因此,HAL是一个简单而强大的链接关系标准,可以很容易地被Angular应用程序消费,很适合选择性地发布遵循领域驱动设计原则的领域模型的业务操作。本文展示的方法可以帮助大大降低Web应用的复杂性,使业务逻辑远离前端,并使前端的行为基于后台的状态而 "恰到好处"。   你可以在这里找到源代码: https://github.com/sth77/spring-angular-ddd-hateoas 。   原文链接:https://www.jdon.com/58628

三位英雄大改,娜可露露大招伤害降低,项羽削弱跌下神坛王者荣耀这款游戏中的英雄调整还是比较重要的,相信很多小伙伴都拥有体验服,这次体验服有三个英雄数据进行调整,首先就是娜可露露,这个英雄在游戏里面出场率还是挺高的,主要就是爆发力比较高(第十期)曹操出装铭文教学在王者峡谷之中,有一个强大的战士,堪比不死魔神,他就是曹操。今天小编给大家推荐一手打野曹操,经过调整之后,曹操的位移距离有所增加,再配上苍穹,那个打不死的男人又回来了。话不多说,先太空冒险射击和声IGN8分有趣且华丽的太空狗斗Fishlabs和DeepSilver的太空设计新作和声已经于12月3日正式发售,IGN今天为本作打出了8分好评,认为游戏为玩家呈现了一场有趣且华丽的太空狗斗。下面让我们一起来看看老OMG衰退原因找到了,柚子小伞互相嫌弃,打野位置从未固定老OMG作为备受欢迎的LPL早期队伍,当年无论在国内还是世界赛上拿到了众多荣誉。那个时候在围绕WE和IG争论不休的时候,OMG一路从资格赛悄然进入LPL,一路连胜了WE和IG,以黑暗黑破坏神不朽还原较为全面,体验较为优秀的CRPG游戏暗黑破坏神不朽还原较为全面,体验较为优秀的CRPG。初代是在暴雪合并了秃鹫之后才完成并发售的,而且暴雪在开发过程中提供了重要的建议和帮助,尤其战网的技术掌握在暴雪手中,我认为将暗黑破坏神不朽死灵法师职业与本次测试简评作者NGA影踪丨小菜菜虽然序幕是以一句你们没有手机吗所展开,相关的故事也已经过去很久,但是暗黑破坏神不朽一直处于大家关注的焦点这一情况依旧延续。姑且不论情怀,暴雪出品必属精品的传奇德杯预选赛霸哥掉线竟逃出生天,腿哥一脸疑惑孤城的迷之操作相信各位英雄联盟的玩家们都知道,自从S11全球总决赛结束之后,国内也是短暂的进入了一段休整期,不过随着时间的推移,我们LPL赛区的冬季转会期即将结束,而一年一度的德玛西亚杯也是即将传奇游戏接上期说到三英雄下冰冻三尺,非一日之寒,大家好我是梦想。当时出台三龙卫正好在边界村民居,他们率先发现了半兽人的行动。当时的笔记城还很弱小,三人只好回神龙帝国求援。他们成功召集了一万大军,可在翻越雪山GT赛车7实机演示视频发布!GT赛车7经典森林赛道演示PS最新独占大作GT赛车7将于2022年3月4日发售,今日索尼公布了一段实机演示视频,展示了系列经典赛道深邃森林,这是一张该系列历代都会出席的传统赛道。一起来看看吧无论您是竞速还是传奇3经典重燃,神舰传说,重拾当年的记忆自2003年传奇3引入国内,就引起了一片热潮,刚开始由光通代理,10年盛大代理运营。传奇3拥有韩国游戏中罕见的立体音效,体现了游戏坚实的功底,是当时最火的网游之一,巅峰时刻更能和热KPL狼队再遇克星TES,向鱼或成突破口,湘军能否再次军训今屿?KPL秋季赛胜者组重庆狼队对阵长沙TES的比赛,可以说是季后赛第一轮的比赛中,胜负最有悬念的一场比赛。主要也是因为两个队伍的全队数据历史交手记录和首发选手的对位数据对比,都相差不大
王者荣耀S15翻盘能力最强的3个法师,练会1个就不怕逆风局!要问王者荣耀之中,哪些法师的翻盘能力最强,第一个要说的就是王昭君。首先这个法师的控制能力超强,很多时候一个二技能就能冰封对方多个英雄。而且她的大招打击力度也大,控制能力也好,哪怕是列王的纷争不善于与人沟通经过我多年的吃亏经验,我终于明白了一件事,那就是与人沟通是非常重要的。如果你不善于与人沟通,那你就会被孤立,就像是我与亮哥联盟的战争。说白了就是我一人对抗他们一个联盟,这也就是他们大神最喜欢的法师,秒杀比妲己强,推塔比周瑜快,天美免费送皮肤要问哪个法师是王者大神的最爱?很多人都会说是妲己或是周瑜,其实他们都差了一点。虽然这二位都有很强的实力,但也只是在顺风局的时候有点作用,要是遇到逆风局那就没用了,几乎是没有翻盘的能王者荣耀S15谁是最强中单法王?谁也想不到米莱狄会封神!S15赛季终于来了,大家现在最关心的是,到底哪个法师才能成为真正的中单法王呢?有人会说是甄姬,也有人会说是貂蝉,其实她们都算不上,真正的中单法王其实是米莱狄。有人看她的出现可能会不大数据公布打坦克最快的射手,伽罗后羿不行,他能秒杀万血程咬金要问王者荣耀之中哪个射手打坦克最快?很多人都会认为是伽罗或是后羿,现在这两个射手也的确厉害,但要说他们打坦克快可能还差了一点。因为有一个射手要比他们还强大,甚至他的攻击都能堪比真实列王的纷争当着你们的面升堡,就问你怕不怕?玩列王的纷争要想战斗力强,那你的技能点就一定要点在军事上,但这样有个弊端,就是发展的要慢一些。毕竟你不可能天天打架,所以很多人都是先把技能点在发展上,这样无论是采集还是产量,或是建列王的纷争国人也有高手当时我也是为了避免有太大的损失,直接就用2000金币把自己的城堡迁走了。不过我也有一点怀疑,为什么那个外国人来的那么及时呢?或者说他也是想抢劫这个联盟,让我先手了?所以他才来打我?王者荣耀影刃最适合吕布?别闹,它才是吕布的神器吕布是王者荣耀之中非常热门的英雄。他之所以会被人追捧,就是因为他的一三技能在命中对方之后的普通攻击,可以百分之百的打出真实伤害!尤其他的真伤还可以暴击,前期也许看不出太强的威力,但闪电匕首和影刃哪个最适合吕布?其实都不行,它才是吕布真正神器本文只代表个人观点前言我想对于吕布出闪电匕首还是出影刃,很多朋友都会有不同的意见。因为这两件装备对于吕布来说,都是有着很强的作用。比如说闪电匕首,它虽然攻击速度有些欠缺,但是它的被王者荣耀中什么英雄小众但适合上分?这些英雄上王者还是很简单的一个英雄小众,无非就两种原因,要么逆版本,要么操作难度高。逆版本的意思就是相对于其他英雄而言更难上分。操作难度高的英雄确实上限高但问题是你会不会。所以要明白,小众是有原因的,如果一英雄联盟手游国服还原做的真讲究!测试体验很丝滑终于等到你!今天终于晚上了英雄联盟手游国服,可算是熬出头了。好家伙,这轮先锋测试开放的名额非常有限且只有安卓渠道,但是玩家的热情让人感觉这就是一款百万热度的超级手游,要知道这只是第