从本章开始,我们将根据电商平台的各个实例项目进行具体的微服务开发,主要包括类目管理、库存管理、订单管理等。在这几个实例项目中,我们将根据项目本身的特点,使用不同的数据库进行开发。对于类目管理来说,我们将使用二级分类设计,即数据实体之间存在一定的关联关系,因此最好的选择就是使用Spring Data JPA进行开发。Spring Data JPA是Spring Boot开发框架中一个默认推荐使用的数据库开发方法,同时,JPA 也是领域驱动设计的一种具体应用。
本章的项目工程可以通过本文的源代码在IDEA中使用Git检出。该项目由三个模块组成:
领域驱动设计(Domain-Driven Design,DDD)是一种面向对象建模,以业务模型为核心展开的软件开发方法。面向对象建模的设计方法,相比于面向过程和面向数据结构的设计方法,从根本上解耦了系统分析与系统设计之间相互隔离的状态,从而提高了软件开发的工作效率。
我们将使用JPA来实现领域驱动设计的开发方法。JPA通过实体定义建立了领域业务对象的数据模型,然后通过使用存储库赋予实体操作行为,从而可以快速进行领域业务功能的开发。
DDD的分层结构
DDD将系统分为用户接口层、应用层、领域层和基础设施层,如图6-1所示。
应用层是很薄的一层,负责接收用户接口层传来的参数和路由到对应的领域层,系统的业务逻辑主要集中在领域层中,所以领域层在系统架构中占据了很大的面积。上下层之间应该通过接口进行通信,这样接口定义的位置就决定了上下层之间的依赖关系。
DDD的基本元素
DDD的基本元素有Entity、Value Object、Service、Aggregate、Repository、Factory、DomainEvent和Moudle等。
JPA(Java Persistence API)即Java持久层API,是Java持久层开发的接口规范。Hibernate、TopLink和 OpenJPA等ORM框架都提供了JPA的实现。Spring Data JPA 的实现使用了Hibernate框架,所以在设计上与直接使用 Hibernate差别不大。但JPA 并不等同于Hibernate,它是在Hibernate之上的一个通用规范。
接下来,我们通过模块catalog-restapi来说明Spring Data JPA的开发方法。
Druid数据源配置
Druid是阿里巴巴开源的一个数据源服务组件,不仅具有很好的性能,还提供了监控和安全过滤的功能。
我们可以创建一个配置类DruidConfiguration来启用Druid 的监控和过滤功能,代码如下所示:
@Configuration
public class DruidConfiguration {
@Bean
public ServletRegistrationBean statviewServle({
ServletRegistrationBean servletRegistrationBean = new
ServletRegistrationBean(new StatViewServlet (), "/druid/*");
//IP地址白名单
servletRegistrationBean.addInitParameter("allow","192.168.0.1,
127.0.0.1");
//IP地址黑名单(共同存在时,deny优先于allow)
servletRegistrationBean.addInitParameter ("deny", "192.168.110.100");//控制台管理用户
servletRegistrationBean.addInitParameter ("loginUsername" , "druid");servletRegistrationBean.addInitParameter ("loginPassword","12345678");//是否能够重置数据
servletRegistrationBean.addInitParameter("resetEnable" ,"false");return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean statFilter(){
FilterRegistrationBean filterRegistrationBean = new
FilterRegistrationBean (new webStatFilter());
//添加过滤规则
filterRegistrationBean.addUrlPatterns("/*");//忽略过滤的格式
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,* .jpg,* .png,*.Css,* .ico,/druid/* ")F
return filterRegistrationBean;
}
}
在使用这个监控配置后,当应用运行时,例如,我们启动catalog-restapi模块,即可通过下列链接打开监控控制台页面:
http://localhost:9095/druid
在登录认证中输入前面代码中配置的用户和密码“druid/12345678”,即可打开如图6-2所示的操作界面。注意,本地的P地址不在前面代码设置的黑名单之中。
在使用这个监控控制台之后,通过查看“SQL监控”的结果,即可为我们对应用的SQL设计和优化提供有价值的参考依据。
我们可以使用项目中的配置文件 application.yml 来设置Druid连接数据源,代码如下所示:
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driverurl:
jdbc:mysql://localhost:3306/catalogdb?characterEncoding=utf8&useSSL=false
username: root
password:12345678
#初始化大小,最小值为5,最大值为120initialSize: 5
minIdle: 5
maxActive: 20
#配置获取连接等待超时的时间maxwait: 60000
#配置间隔多久进行一次检测,检测需要关闭的空闲连接,单位是mstimeBetweenEvictionRunsMillis: 60000
#配置一个连接在池中最小生存时间,单位是msminEvictableIdleTimeMillis:300000validationQuery: SELECT 1 FROM DUALtestWhileIdle: true
testOnBorrow: falsetestOnReturn: false
#打开PSCache,指定每个连接上 PSCache的大小poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
#配置监控统计拦截的filters,如果去掉,则监控界面SQL将无法统计, 'wall'用于防火墙filters: stat, wall, log4j
#通过connectProperties属性打开mergeSql功能;慢sQL记录connectionProperties:
druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
在上面的配置中,主要设定了所连接的数据库,以及数据库的用户名和密码,确保数据库的用户配置信息正确,并且具有读写权限。其他一些配置可由 Druid 的通用参数来设定。
数据源的配置同时适用于MyBatis的开发。
JPA 初始化和基本配置
首先,我们新建一个配置类JpaConfiguration,初始化一些JPA的参数,代码如下所示:
econfiguration
@EnableTransactionManagement (proxyTargetClass = true)
@EnableJpaRepositories (basePackages = "com.** .repository")CEntityScan(basePackages = "com.* *.entity")
public class JpaConfiguration{
@Bean
PersistenceExceptionTranslationPostProcessorpersistenceExceptionTranslationPostProcessor(){
return new PersistenceExceptionTranslationPostProcessor();
}
}
在这里,我们设置存储库的存放位置为“com.**.repository”,同时设置实体的存放位置为“com.**.entity”,这样就能让JPA找到我们定义的存储库和实体对象了。
然后,在应用程序的配置文件中,增加如下配置:
spring:
jpa:
database: MYSQLshow-sql: false
## Hibernate ddl auto (validate lcreate l create-drop l update)hibernate:
ddl-auto: update
#naming-strategy:org.hibernate.cfg. ImprovedNamingStrategynaming.physical-strategy:
org.hibernate.boot.model.naming. PhysicalNamingStrategystandardImpl
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5Dialectenable_lazy_load_no_trans: true
其中,“ddl-auto”设置为“update”,表示当实体属性更改时,将会更新表结构。如果表结构不存在,则创建表结构。注意,不要把“ddl-auto”设置为“create”,否则程序每次启动时都会重新创建表结构,而之前的数据也会丢失。如果不使用自动功能,则可以设置为“none”。上面配置中的最后一行代码开启了Hibernate的延迟加载功能,这可以提高关联关系查询时的访问性能。
在使用Spring Data JPA进行实体建模时,主要使用Hibernate的对象关系映射(ORM)来实现。在类目管理项目中我们需要创建两个实体,分别为主类和二级分类。
主类由名称、操作者和创建日期等属性组成,实现代码如下所示:
@Entity
@Table(name ="tsorts")
public class Sorts implements java.io.Serializable {
CId
@Generatedvalue(strategy = GenerationType. IDENTITY)private Long id;
private string name;
private string operator;
@DateTimeFormat (pattern = "Yyyy-MM-dd HH:mm:ss")
eColumn (name = "created",columnDefinition = "timestamp defaultcurrent timestamp")
@Temporal (TemporalType.TIMESTAMP)private Date created;
coneToMany(cascade = CascadeType.REMOVE)@OrderBy("created asc")
CJoincolumn (name= "sorts id")
private Set<Subsorts> subsortses = new HashSet<>();
从上面代码中可以看出,我们使用了表“t_sorts”来存储数据,并且它与二级分类以一对多的方式建立了关联关系。建立关联关系的是“sorts_id”,它将被保存在二级分类的表格中。另外,在查询这种关系时,我们指定了以创建时间“created”进行排序。
二级分类实体由名称、操作者和创建日期等属性组成,代码如下所示:
@Entity
@Table(name ="tsubsorts")
public class Subsorts implements java.io.Serializable t
@Id
@Generatedvalue(strategy = GenerationType. IDENTITY)private Long id;
private string name;
private String operator;
@DateTimeFormat (pattern = "Yyyy-MM-dd HH:mm:ss")
@Column (name = "created",columnDefinition = "timestamp defaultcurrent_timestamp")
@Temporal (TemporalType.TIMESTAMP)private Date created;
...
}
二级分类使用了表结构“t_subsorts”来存储数据,字段定义与主类的定义几乎相同。
在上面两个实体对象的设计中,我们通过主类使用一对多的方式与二级分类实现关联设计,这样,当在主类中进行查询时,将可以同时获取二级分类的数据;而对主类的存储和更新,也将自动涉及分类的相关操作。
有关实体建模的设计,特别是关联关系的设计,我们主要说明以下几个重要的功能。
(1)实体对象必须有一个唯一标识。
这里使用Long类型定义对象的身份标识“id”,并且这个“id”将由数据库自动生成。在实际应用中,推荐使用UUID作为对象的唯一标识,这样不仅可以保持这一字段长度的一致性,还能保证这一标识在整个数据库中的唯一性,而且还将非常有利于数据库的集群设计。
(2)日期属性要使用正确的格式。
使用注解“@DateTimeFormat”对日期进行格式化,不仅可以保证日期正常显示,还能保证在参数传递中日期的正确性。注意,上面的创建日期“created”使用了默认值设置。
(3)使用合理的关联设置。
关联设置是实体设计的关键,为了避免引起递归调用,最好使用单向关联设置,即在互相关联的两个对象之中,只在一个对象中进行关联设置。
一般来说,多对多的关联可以使用中间表来存储关联关系,而一对多或多对一的关联关系可以使用一个字段来存储关联对象的外键。例如,在上面的实体设计中,我们使用“sorts_id"作为二级分类与主类关联的外键。
在主类实体的关联设置中,我们还使用了级联的操作设置:“CascadeType.REMOVE”。这样,当主类中的一个类别被删除时,将会自动删除与其关联的所有分类。
有关级联的设置,可以使用的选项如下所示:
我们将查询对象设计放在一个公共模块catalog-object中,这样,其他两个模块都可以进行调用。使用查询对象(Query Object,qo)是为了与vo进行区分。有人把vo看成值对象(ValueObject),也有人把vo看成视图对象(View Object),所以很容易引起误解。这两种对象的意义和用途是不一样的,值对象表示的是与实体不同的一些数据,它可以作为视图显示;而视图对象是只能作为视图显示的一种数据。
因为实体是有生命周期和状态的,并且它的状态会直接影响存储的数据,所以我们使用一个无状态的数据对象来存储实体的数据。这些数据的使用和更改不会直接影响数据存储,因此它的使用是安全的,也可以用之即弃。
我们既可以将查询对象作为值对象使用,也可以将查询对象作为视图对象使用,还可以将查询对象作为查询参数的一个集合来使用,即相当于一个数据传输对象(Data Transfer Object, dto)。
我们只要使用一个查询对象qo,就可以包含vo、dto等对象的功能,这是一种简化设计。qo有时会包含一些冗余数据,但这对于使用方来说影响不大。例如,在我们的查询对象中,将会包含分页所需的页码和页大小等分页属性数据,而在视图显示中并不需要这些数据,所以它可以不用理会这些数据。
相对于主类实体,它的查询对象的设计如下所示:
public class SortsQ0 extends PageQ0 {
private Long id;
private String name;
private String operator;
@DateTimeFormat (pattern = "yyyY-MM-dd HH:mm :ss")private Date created;
private List<SubsortsQ0> subsortses = new ArrayList<>();
...
}
其中,它所继承的PageQo查询对象将提供两个分页查询参数,实现代码如下所示:
public class PageQ0 [
private Integer page = 0;private Integer size = 10;
...
}
在分页参数中,只有一个页码和每页大小的设定两个字段。
使用JPA进行实体数据持久化设计是比较容易的,只要为实体创建一个存储库接口,将实体对象与JPA的存储库接口进行绑定,就可以实现实体的数据持久化设计,相当于给实体赋予了一些访问数据库的操作行为,包括基本的增删改查等操作。
除数据存储的基本操作外,我们还可以根据实体的字段名称来声明查询接口,而对于一些复杂的查询,也可以使用SQL查询语言设计。实体主类的存储接口设计如下所示:
@Repository
public interface SortsRepository extends JpaRepository<Sorts,Long>,JpaSpecificationExecutor<Sorts> {
Page<Sorts> findByNameLike (@Param ( "name ") string name,PageablepageRequest);
@Query("select t from Sorts t where t.name like :name and t.created=:created")
Page<Sorts> findByNameAndCreated(@Param ("name") String name,@Param ("created") Date created, Pageable pageRequest);
Sorts findByName (@Param("name") String name);
@Query ( "select s from Sorts s"+
"left join s.subsortses b"+"where b.id= :id")
Sorts findBySubsortsId(@Param( "id") Long id);
}
这个接口定义是不用我们实现的,只要方法定义符合JPA的规则,后续的工作就可以交给JPA来完成。
在JPA中,可以根据以下方法自定义声明方法的规则,即在接口中使用关键字findBy.readBy、getBy等作为方法名的前缀,然后拼接实体类中的属性字段(首个字母大写),最后拼接一些SQL查询关键字(也可不拼接),组成一个查询方法。下面是一些查询关键字的使用实例:
通过注解@Query使用SQL查询语言设计的查询,基本与数据库的查询相同,这里只是使用实体对象的名字代替了数据库表的名字。
在上面的存储库接口定义中,我们不但继承了JPA的基础存储库JpaRepository,还继承了一个比较特别的存储库JpaSpecificationExecutor,通过这个存储库可以进行一些复杂的分页设计。
前面的持久化设计已经在实体与数据库之间建立了存取关系。为了更好地对外提供数据访问服务,我们需要对存储库的调用再进行一次封装。在这次封装中,我们可以实现统一事务管理及其分页的查询设计。分类的数据管理服务设计代码如下所示:
@Service
@Transactional
public class SortsService {
@Autowired
private SortsRepository sortsRepository;
public Sorts findOne (Long id){
Sorts sorts =sortsRepository.findById(id).get();return sorts;
public Sorts findByName (string name){
return sortsRepository.findByName (name) ;
}
public String save (Sorts sorts){
tryi
sortsRepository.save(sorts);
return sorts.getId() .toString();}catch(Exception e){
e.printStackTrace();return e.getMessage();
}
public String delete (Long id){
try{
sortsRepository.deleteById(id);return id.toString();
Jcatch(Exception e){
e.printStackTrace();return e.getMessage ();
public Page<Sorts> findAll (SortsQo sortsQo){
Sort sort = new Sort (Sort.Direction. DESC, "created");
Pageable pageable = PageRequest.of (sortsQo.getPage (), sortsQo.getSize(),
sort);
return sortsRepository.findAll (new Specification<Sorts>()1
@override
public Predicate toPredicate (Root<Sorts> root, CriteriaQuery<?>query,
CriteriaBuilder criteriaBuilder) {
List<Predicate> predicatesList =new ArrayList<Predicate>();
if(CommonUtils.isNotNull (sortsQo.getName()){
predicatesList.add (criteriaBuilder.like(root.get ("name"),
"号"+sorts0o.getName()+"%"));
}
if(CommonUtils.isNotNull (sortsQo.getCreated())){
predicatesList.add (criteriaBuilder.greaterThan (root.get ("created"),sortsQo.getCreated()));
}
query.where(predicatesList.toArray (new
Predicate[predicatesList.size()]));
return query.getRestriction();
}, pageable);
}
}
在上面的代码中,使用注解@Transactional 实现了隐式事务管理,对于一些基本的数据操作,可直接调用存储库接口的方法。
在上述代码中,使用findAll方法实现了分页查询的设计。在这个设计中,可以定义排序的方法和字段,以及对页码和每页行数的设定,同时,还可以根据查询参数动态地设置查询条件。在这里,我们既可以按分类的名称进行模糊查询,也可以按分类的创建时间进行限定查询。
在完成上节的设计之后,我们可以写一个测试用例验证领域服务的设计。需要注意的是,因为在前面的JPA配置中已经有了更新表结构的配置,所以如果表结构不存在,则会自动生成;如果表结构更新,则启动程序也会自动更新。下面的测试用例演示了如何插入分类和主类的数据:
@RunWith(SpringJUnit4ClassRunner.class)
eContextConfiguration (classes = {UpaConfiguration.class,SortsRestApiApplication.class})
@SpringBootTest
public class SortsTest{
private static Logger logger = LoggerFactory.getLogger (SortsTest.class);
CAutowired
private SortsService sortsService;@Autowired
private SubsortsService subsortsService;
@Test
public void insertData() {
Sorts sorts =new Sorts();sorts.setName("图书");
sorts.setOperator( "editor");sorts.setCreated(new Date());
//
Sorts sorts = sortsService.findByName("图书");
Subsorts subsorts = new Subsorts();subsorts.setName("计算机");
subsorts.setOperator("editor");subsorts.setCreated(new Date());
subsortsService.save (subsorts);
Assert.notNull(subsorts.getId(), "insert sub error");
sorts.addSubsorts(subsorts);
sortsService.save(sorts);
Assert.notNull (sorts.getId(), "not insert sorts");
}
...
}
其他查询的测试用例可以参照这个方法设计,如果断言没有错误,则说明测试符合预期,即不会提示任何错误信息。在调试环境中,还可以借助控制台信息分析测试的过程。
类目接口微服务是一个独立的微服务应用,它将使用基于REST 协议的方式,对外提供一些有关类目查询和类目数据管理的接口服务。这个接口服务,既可以用于商家后台进行类目管理的设计之中,也可以用于移动端、App或其他客户端程序的设计之中。
当上面的单元测试完成之后,我们就可以使用上面设计中提供的数据服务进行类目接口微服务的开发了。
RESTful接口开发
我们将遵循REST协议的规范设计基于RESTful的接口开发,例如,对于分类来说,我们可以设计如下请求:
下面的代码展示了分类接口设计的部分实现,完整的代码可以查看项目工程的相关源代码:
@RestController
@RequestMapping("/sorts")
public class SortsController {
private static Logger logger = LoggerFactory.getLogger(SortsController.class);
@Autowired
private SortsService sortsService;
@GetMapping (value="/{id] ")
public String fnidById(@PathVariable Long id){
return new Gson().toJson (sortsService.findOne (id));
)
@GetMapping ()
public String findAll(Integer index,Integer size, String name)
try {
SortsQo sortsQ0 = new SortsQ0();if(CommonUtils.isNotNul1(index)){
sortsQo .setPage(index);
}
if(CommonUtils.isNotNull(size)){
sortsQ0.setsize(size);
}
if(CommonUtils.isNotNul1 (name)){
sortsQo. setName(name);
}
Page<Sorts> orderses = sortsService.findAll(sortsQo);
Map<String, 0bject> page = new HashMap<>();
page.put( "content", orderses.getContent();
page.put ("totalPages", orderses.getTotalPages());
page.put ("totalelements",orderses.getTotalElements());
return new Gson() .toJson (page);
}
Jcatch(Exception e){
e.printStackTrace();
}
return null;
)
@PostMapping()
public String save (CRequestBody SortsQo sortsQo) throws Exception{t
Sorts sorts =new sorts();
BeanUtils.copyProperties (sortsQ0,sorts);sorts.setCreated (new Date());
List<Subsorts> subsortsList = new ArrayList<>();//转换每个分类,然后加入主类的分类列表中
for(SubsortsQo subsortsQ0 : sortsQo.getSubsortses()){
Subsorts subsorts =new Subsorts();
BeanUtils.copyProperties(subsortsQ0,subsorts);subsortsList.add(subsorts);
)
sorts. setSubsortses (subsortsList);
String ret =sortsService.save(sorts);logger.info("新增="+ ret);
return ret;
}
...
}
在上面微服务接口设计中,使用RestController 定义了对外提供服务的URL 接口,而接口之中有关数据的访问则通过调用SortsService的各种方法来实现。其中,在接口调用中,都使用JSON方式的数据结构来传输数据,所以在上面代码中,显式或隐式地使用了JSON 的数据结构。对于一个数据对象来说,为了保证其数据的完整性,我们一般使用GSON 工具对数据进行显式转换。
需要注意的是,因为在数据传输中使用的是查询对象,所以当进行数据保存和更新操作时,需要将查询对象转换为实体对象。
微服务接口调试
当微服务接口开发完成之后,即可启动项目的应用程序进行简单调试。对于类目微服务接口,我们可以启动catalog-restapi模块中的主程序SortsRestApiApplication进行调试。
在启动成功之后,对于一些GET请求,可以直接通过浏览器进行调试。
例如,通过下列链接地址,可以根据分类ID查看一个分类的信息:
http://localhost:9091/sorts/1
如果数据存在,则返回如图6-3所示的JSON数据。
使用如下链接地址可以查询分页第一页的数据:
http://localhost:9091/sorts
如果查询成功,则可以看到如图6-4所示的信息。
因为POST 和 PUT等请求在调试时需要传输参数,所以不能直接使用浏览器进行测试,但是可以通过Postman等工具进行调试。
我们可以使用多种方法调用基于RESTful接口的服务。例如,可以使用HTTP 访问(例如HttpClient),或者使用RestTemplate的方式进行调用,等等。但是,在微服务应用中,最好的方法是使用声明式的FeignClient。
因为 FeignClient是为其他微服务进行调用的,所以这里将这些设计都放在模块catalog-object中进行开发。
声明式FeignClient 设计
FeignClient是一个声明式的客户端,为了使用这个工具组件,我们需要在项目对象模型中引入 FeignClient的依赖,代码如下所示:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
针对主类的接口调用,我们可以定义一个接口程序SortsClient,根据微服务catalogapi提供的接口服务,使用如下所示的方法声明一些调用方法:
@FeignClient ("catalogapi")
public interface SortsClient {
@RequestMapping (method = RequestMethod.GET, value = "/sorts/{id}")String findById(CRequestParam("id") Long id);
@RequestMapping (method = RequestMethod.GET,value = "/sorts/findAll")String findList();
CRequestMapping (method = RequestMethod.GET, value = "/sorts",
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
produces =MediaType.APPLICATION_JSON_UTF8_VALUE)
String findPage (CRequestParam("index") Integer index, @RequestParam ("size")Integer size,
@RequestParam( "name") String name);
@RequestMapping (method =RequestMethod.GET, value = "/sorts/findAll",
consumes = MediaType.APPLICATION JSON UTF8_VALUE,
produces = MediaType.APPLICATION_ JSON_UTF8_VALUE)
String findAll();
@RequestMapping (method = RequestMethod. POST, value = "/sorts",
consumes = MediaType.APPLICATION_JSON UTF8 VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
String create(@RequestBody SortsQ0 sortsQ0) ;
@RequestMapping (method = RequestMethod.PUT,value = "/sorts",
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
produces = MediaType.APPLICATION_JSON_ UTF8_VALUE)
String update(@RequestBody SortsQo sortsQo);
@RequestMapping (method = RequestMethod. DELETE,value = "/sorts/{id}")String delete (@RequestParam("id") Long id);
}
在这个实现代码中,首先通过注解@FeignClient引用微服务catalogapi,然后使用其暴露出来的URL直接声明调用方法。需要注意的是,这里的数据传输,即数据的生产和消费,都是通过JSON格式进行的,所以为了保证中文字符的正确性,我们使用UTF8编码。
断路器的使用
基于SortsClient的声明方法,我们可以创建一个服务类SortsRestService进行调用。然后,使用SortsRestService提供的功能,就可以像使用本地方法一样使用微服务catalogapi 提供的接口方法。服务类SortsRestService的实现代码如下所示:
@service
public class SortsRestService {
CAutowired
private SortsClient sortsClient;
@HystrixCommand (fallbackMethod = "findByIdFallback")public String findById(Long id){
return sortsClient.findById(id);
private String findByIdFal1back (Long id){
SortsQo sortsQo = new SortsQ0();
return new Gson() .toJson (sortsQo);
}
...
}
在上面的代码中,我们实现了对SortsClient的调用,同时增加了一个注解@HystrixCommand。通过这个注解,定义了一个回退方法。而这一回退方法的设计,就是SpringCloud组件提供的断路器功能的实现方法。断路器的含义是,当服务调用过载或不可用时,通过降级调用或故障转移的方法,减轻服务的负载。这里我们使用了回退方法设计,以快速响应来自客户端的访问,并保障客户端对微服务的访问不会因为出现故障而崩溃。断路器的设计就像电路的保护开关一样,对系统服务起到一定的保护作用。与保护开关不同的是,当系统恢复正常时,断路器会自动失效,不用人为干预。
这里 类目管理是一个基于 PC 端的 Web 应用,它也是一个独立的微服务应用。这个应用在项目工程的模块catalog-web 中实现,可以把它看成一个独立的项目。
在这个应用中,我们将演示如何使用类目管理微服务接口提供的服务,进行相关应用功能的开发,从而实现在PC端提供一个对类目进行操作管理的友好操作界面。
接口调用引用的相关配置
上面的接口调用服务是在模块catalog-object 中进行开发的,想要在模块“catalog-web”中使用这些服务,就必须先在项目对象模型中进行引用配置,代码如下所示:
<dependency>
<groupId>com.demo</groupId>
<artifactId>catalog-object</artifactId><version>${project.version}</version></dependency>
因为两个模块处于同一个项目工程之中,所以上面引用配置的版本直接使用了项目的版本。这样,当接口服务启动之后,我们就可以在接下来的 Web应用中进行相关调用了。
需要注意的是,如果有多个FeignClient程序调用了同一个微服务接口服务,则必须在项目的配置文件中使用如下所示的配置进行设置,以支持这种调用方式。因为这个Spring Cloud版本的默认配置是不开启这种调用方式的:
#允许多个接口使用相同的服务
spring:
main:
allow-bean-definition-overriding: true
Spring MVC控制器设计
Spring MVC是 Web应用开发的一个基础组件,下面我们使用这一组设计一个控制器。在Web应用的主类控制器设计中,我们直接使用上面设计的服务类:SortsRestService。我们可以像使用本地方法一样使用SortsRestService类,直接调用微服务提供的接口服务,代码如下所示:
@GRestController
@RequestMapping ( "/sorts")
public class SortsController {
private static Logger logger =
LoggerFactory.getLogger(SortsController.class);
@Autowired
private SortsRestService sortsRestService;
@GetMapping(value=" /index")
public Mode1AndView index(){
return new ModelAndview( "sorts/index");
@GetMapping (value="/{id]")
public ModelAndView findById(@PathVariable Long id){
return new ModelAndView("sorts/show", "sorts",
new Gson() .fromJson (sortsRestService.findById(id),
SortsQo.class));
}
...
}
上面代码中的findByld方法是一个使用页面来显示分类信息的设计。在这个设计中,一方面引用了上面设计的服务类SortsRestService,并调用了它的findByld 方法,进行数据查询;另一方面将查询数据通过一个 show页面显示出来。这个设计与一般的本地调用不同的是,查询数据时得到的返回值是一种ISON结构,所以必须将它转化为一个查询对象,这样才能方便使用。
接下来的页面设计将会用到Thymeleaf模板的功能。
在 Web应用的页面设计中,我们将使用Thymeleaf 这个模板,因此,必须在catolog-web模块中引入Thymeleaf 的依赖,代码如下所示:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId><version>2.3.0</version>
</dependency>
有关 Thymeleaf 的配置,使用其默认配置即可,即只要在程序的资源目录中有static和templates这两个目录就可以了。这两个目录分别用来存放静态文件和模板设计及其页面设计文件,页面文件的后缀默认使用html。
HTML页面设计
在6.10节控制器的设计中,类目信息输出的是一个show页面,它的设计在show.html文件中,代码如下所示:
<html xmlns:th="http://www.thymeleaf.org"><div class="addInfBtn">
<h3 class="itemTit"><span>类目信息</span></h3><table class="addNewInfList">
<tr>
<th>名称</th>
<td width="240"><input class="inp-list w-200 clear-mr f-left"
type="text" th:value="$ {sorts.namel" readonly="true"/></td>
<th>操作者</th>
<td><input class="inp-list w-200 clear-mr f-left" type="text"
th:value="$ {sorts.operator}" readonly="true"/></td>
</tr>
<tr>
<th>子类</th><td>
<select multiple= "multiple" readonly="true">
<option th:each="subsorts:${sorts.subsortses] "
th:text="${#strings. length(subsorts.name)
>20?#strings.substring (subsorts.name,0,20)+'...':subsorts.name} "
th:selected="true"
></option>
</select>
</td>
<th>日期</th><td>
<input onfocus="WdatePicker ({dateFmt:'yyyy-MiM-dd HH :mm:ss'))"
type="text" class="inp-list w-200 clear-mr f-left" th:value="${sorts.created)?$ {#dates.format(sorts.created, 'vyvy-MM-dd HlH:mm:ss')}:''" readonly="true"/>
</td>
</tr></table>
<div class="bottomBtnBox">
<a class="btn-93x38 backBtn" href="javascript:closeDialog (0)">返回</a></div>
</div>
从上面的代码可以看出,除用到Thymeleaf特有的地方外,其他设计都与一般的HTML标签语言相同。设计之后,这个页面的最终效果如图6-5所示。
统一风格模板设计
Thymeleaf更强大的功能是提供了一个统一风格的模板设计,即整个网站可以使用统一风格的框架结构。在类目管理这个项目中,使用了总体页面框架设计layout.html,代码如下所示:
<! DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout">
<body>
<div class="headerBox">
<div class="topBox">
<div class="topLogo f-left">
<a href="#"><img th:src="@{/images/logo.pngl "/></a></div>
</div></div>
<div class="locationLine" layout:fragment="prompt">
当前位置:首页 > <em>页面</em>
</div>
<table class="globalMainBox" style="position:relative;z-index:1">
<tr>
<td class="columnLeftBoX" valign="top">
<div th:replace="fragments/nav ::nav"></div></td>
<td class="whiteSpace"></td>
<td class="rightColumnBox" valign="top"><div layout: fragment="content"></div></td>
</tr></table>
<form th:action="@{/logout}" method="post" id="logoutform"></form>
<div class="footBox" th:replace="fragments/footer :: footer"></div></body>
</html>
页面上方是状态栏,页面左侧是导航栏,中间部分是内容显示区域,底端还有一个页脚设计。在引用这个模板之后,只需对需要更改的区域进行覆盖就可以了,而不需要更改的地方使用模板的默认设计即可。一般来说,在使用这个模板时,只要更改状态栏和内容显示区域就可以了,而导航栏和页脚,则可以使用通用的页面设计。
在这个例子中,分类的主页是通过index.html这个页面设计来引用这个模板的,代码如下所示:
<! DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout"
layout:decorator="fragments/layout">
<body>
<!--状态栏-->
<div class="locationLine" layout: fragment="prompt">
当前位置:首页> <em >类目管理</em>
</div>
<!--主要内容区域-->
<div class="statisticBoX w-782"layout:fragment="content">
...
</div></body></html>
可以看出,在上面的代码中,我们只更新了状态栏和主要内容显示区域的设计,其他部分都沿用了模板的设计。
在上面的一些设计讲解和演示中,我们只说明了主类的设计,二级分类的设计与主类的设计大同小异,不再赘述。
至此,类目管理的微服务应用的开发工作就基本完成了。
现在我们可以体验微服务之间的调用了,因为使用了Spring Cloud工具组件来开发,所以在各个方面的实现都是非常方便的。当然,对于微服务的调用,不仅仅是Web应用的调用,还有其他如App应用、微信公众号或小程序客户端,或者其他语言的设计、异构环境的调用,等。不管使用哪种工具来设计,只要能用HTTP,就可以轻易实现对微服务的调用。
在类目管理的微服务接口及其Web微服务应用都开发完成之后,我们就可以进行一个总体测试了。首先确认Consul已经运行就绪,然后先后启动catalog-restapi和 catalog-web两个模块。启动成功之后,通过浏览器访问如下链接地址:
http://localhost:8091
如果一切正常,则可以进入如图6-6所示的类目管理的主页。在这里,我们可以分别对主类和二级分类中的所有类目进行增删改查的所有操作。
在使用IDEA开发工具执行打包时,可以使用 Maven项目管理器执行打包操作,如图6-7所示。
如果是模块化的项目,请务必在项目的根(root)目录中执行打包操作,这样才能将其所依赖的模块同时打包在一起。
当打包完成之后,可以使用命令终端,分别切换到catalog-restapi和 catalog-web模块的 target目录中执行下列命令,启动应用进行调试:
java -jar catalog*.jar
以这种方式启动应用,与上面使用IDEA工具进行调试时的效果是一样的。如果启动正常,则可以进行与上面一样的测试。
这种启动方式也可以作为一种普通的方式来发布微服务,在生产环境中,可以在上面指令的基础上增加一些内存和日志存储方面的参数。
有关微服务应用的部署,将在运维部署部分进行详细介绍。
本章介绍了电商平台的类目管理接口和Web类目管理后台两个微服务的开发实例,通过这个项目的开发和演示,我们清楚了微服务之间快速通信和相互调用的方法。在类目管理接口开发中,我们通过Spring Data JPA开发工具,了解了DDD开发方法在Spring 开发框架中的工作原理和实现方法。通过类目管理接口的实现,我们将有状态的数据访问行为,转变成没有状态的接口服务。
下一章,我们将介绍另一种数据库开发工具 MyBatis,体验不同的数据库开发工具在Spring项目工程中的应用方法。
本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。