应用开发框架 - 项目开发说明

项目介绍

公共类库

common,提供公用的工具类、框架代码

业务类库

以 业务领域 为单位,实现 业务领域、业务接口,如:

  • base,基础功能
  • system,系统功能
  • biz,业务示例

每个 业务领域项目下,又分为 领域层 domain、接口层 api

业务类库中,尽量以通用服务的方式实现业务

微服务项目 sa

将 业务类库 包装为 微服务,对外提供 RESTful API,以及相关的接口文档的访问、测试

后端项目 bff

提供 面向UI 的后端接口

对服务接口的转发(建议,避免使用)

对服务接口的聚合、裁剪、适配

提供用户认证,保护接口的访问权限

项目开发

业务类库

面对业务需求,设计时,进行领域划分,保证划分的粒度适中

  • 在 领域层 domain,创建 实体 entity、数据传输对象 dto、持久化 repo、业务逻辑 service

  • 在 接口层 api,创建 值对象 vo、接口 api

实体 entity

package com.supwisdom.institute.backend.biz.domain.entity;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

import com.supwisdom.institute.backend.common.framework.entity.ABaseEntity;

@Entity
@Table(name = "TB_BIZ")
public class Biz extends ABaseEntity {

  /**
   * 
   */
  private static final long serialVersionUID = 5503233707196628811L;
  
  @Getter
  @Setter
  @Column(name = "NAME")
  private String name;
  
  @Getter
  @Setter
  @Column(name = "BOOL")
  private Boolean bool;
  
  @Getter
  @Setter
  @Column(name = "DATE")
  private Date date;

  @Getter
  @Setter
  @Column(name = "NUM")
  private Integer num;

}

继承 ABaseEntity

注解 @Entity,指定该 class 为一个实体

注解 @Table,指定该实体对应的数据库表的表名

注解 @Column,指定该属性对应的数据库表的字段名

表名、字段名 都采用大写,多个单词用 下划线(_) 隔开

数据传输对象 dto


持久化 repo

package com.supwisdom.institute.backend.biz.domain.repo;

import java.util.Map;

import org.springframework.data.domain.Page;
import org.springframework.stereotype.Repository;

import com.supwisdom.institute.backend.biz.domain.entity.Biz;
import com.supwisdom.institute.backend.common.framework.repo.BaseJpaRepository;

@Repository
public interface BizRepository extends BaseJpaRepository<Biz> {
  
  @Override
  public default Page<Biz> selectPageList(boolean loadAll, int pageIndex, int pageSize, Map<String, Object> mapBean, Map<String, String> orderBy) {
    
    
    return null;
  }

}

继承 BaseJpaRepository<E>,基类中,已经实现了基本的 CURD 逻辑,方法如下:

  • 分页查询
  public default Page<E> selectPageList(boolean loadAll, int pageIndex, int pageSize, Map<String, Object> mapBean, Map<String, String> orderBy)
  • 根据ID获取
  public default E selectById(String id)
  • 新增
  public default E insert(E entity)
  • 更新
  public default E update(E entity)
  • 删除
  public default void delete(String id)

注解 @Repository,指定该 class 为一个数据持久化类

其中 BaseJpaRepositoryselectPageList 方法,默认返回所有数据,不会处理查询条件、排序,故一般都需要由业务自行重写

  default Specification<Biz> convertSpecification(Map<String, Object> mapBean) {

    Specification<Biz> spec = new Specification<Biz>() {

      /**
       * 
       */
      private static final long serialVersionUID = -1820403213133310124L;

      @Override
      public Predicate toPredicate(Root<Biz> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        List<Predicate> predicates = new ArrayList<>();
        
        if (mapBean != null) {
          if (!StringUtils.isEmpty(MapBeanUtils.getString(mapBean, "name"))) {
            predicates.add(criteriaBuilder.like(root.get("name"), MapBeanUtils.getString(mapBean, "name")));
          }

          if (!StringUtils.isEmpty(MapBeanUtils.getBoolean(mapBean, "bool"))) {
            predicates.add(criteriaBuilder.equal(root.get("bool"), MapBeanUtils.getBoolean(mapBean, "bool")));
          }
          
          if (!StringUtils.isEmpty(MapBeanUtils.getString(mapBean, "dateBegin"))) {
            String grantTimeBegin = MapBeanUtils.getString(mapBean, "dateBegin");
            Date d = DateUtil.parseDate(grantTimeBegin+" 00:00:00", "yyyy-MM-dd HH:mm:ss");
            
            if (d != null) {
              predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("date"), d));
            }
          }

          if (!StringUtils.isEmpty(MapBeanUtils.getString(mapBean, "dateEnd"))) {
            String grantTimeEnd = MapBeanUtils.getString(mapBean, "dateEnd");
            Date d = DateUtil.parseDate(grantTimeEnd+" 23:59:59", "yyyy-MM-dd HH:mm:ss");
            
            if (d != null) {
              predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("date"), d));
            }
          }
        }
        
        return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
      }
      
    };
    
    return spec;
  }
  
  @Override
  public default Page<Biz> selectPageList(boolean loadAll, int pageIndex, int pageSize, Map<String, Object> mapBean, Map<String, String> orderBy) {
    
    Specification<Biz> spec = this.convertSpecification(mapBean);

    if (loadAll) {
      pageIndex = 0;
      pageSize = Integer.MAX_VALUE;
    }
    
    Sort sort = new Sort(Sort.Direction.DESC, "date");  // Sort.unsorted

    if (orderBy != null) {
      List<Order> orders = new ArrayList<>();
      
      orderBy.forEach((k, v) -> {
        if ("asc".equalsIgnoreCase(v)) {
          Order order = Order.asc(k);
          orders.add(order);
        } else if ("desc".equalsIgnoreCase(v)) {
          Order order = Order.desc(k);
          orders.add(order);
        } else {
          Order order = Order.by(k);
          orders.add(order);
        }
      });
      
      sort = Sort.by(orders);
    }

    PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sort);
    
    return this.findAll(spec, pageRequest);
  }

业务逻辑 service

package com.supwisdom.institute.backend.biz.domain.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.supwisdom.institute.backend.biz.domain.entity.Biz;
import com.supwisdom.institute.backend.biz.domain.repo.BizRepository;
import com.supwisdom.institute.backend.common.framework.service.ABaseService;

@Service
public class BizService extends ABaseService<Biz, BizRepository> {
  
  @Autowired
  private BizRepository bizRepository;

  @Override
  public BizRepository getRepo() {
    return bizRepository;
  }

}

继承 ABaseService<E, REPO>

注解 @Service,指定该 class 为一个业务逻辑类

注入 业务对应的 Repository,并实现 public REPO getRepo()

值对象 vo

这里的vo,目前主要用于接口的请求、响应对象的封装

接口 api

package com.supwisdom.institute.backend.biz.api.v1.admin;

import io.swagger.annotations.Api;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(value = "BizAdminBiz", tags = { "BizAdminBiz" }, description = "Biz示例接口")
@RestController
@RequestMapping("/v1/admin/biz")
public class AdminBizController {

}

微服务项目

  • 关于 Application
package com.supwisdom.institute.backend.admin.sa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import com.supwisdom.infras.online.doc.configuration.EnableInfrasOnlineDoc;
import com.supwisdom.institute.backend.common.core.transmit.annotation.EnableSimpleUserTransmit;
import com.supwisdom.institute.backend.common.framework.exception.EnableCustomExceptionHandler;

@SpringBootApplication

@EnableSimpleUserTransmit
@EnableCustomExceptionHandler

@EnableInfrasOnlineDoc

@EntityScan(basePackages = {"com.supwisdom.**.domain.entity"})  // 扫描子项目下的实体
@EnableJpaRepositories(basePackages = {"com.supwisdom.**.domain.repo"})  // 扫描子项目下的持久类
@ComponentScan(basePackages = {"com.supwisdom"})
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
  
  @Bean
  public CorsFilter corsFilter() {
    final CorsConfiguration config = new CorsConfiguration();
    // config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/v2/api-docs", config);

    return new CorsFilter(source);
  }

}

注解 @SpringBootApplication

注解 @EnableSimpleUserTransmit,从请求中接收 User 信息,通过 Feign 调用外部服务时,传递 User 信息

注解 @EnableCustomExceptionHandler,将异常转换为符合开发规范的 json 数据

注解 @EntityScan,扫描实体

注解 @EnableJpaRepositories,扫描持久类

注解 @ComponentScan,扫描组件,如 @Service、@Controller、@RestController 等

Filter corsFilter,允许跨域请求,此处允许对路径 /v2/api-docs 的跨域请求

  • 在 pom 中,添加依赖配置
    <dependency>
      <groupId>com.supwisdom.institute</groupId>
      <artifactId>sw-backend-biz-api</artifactId>
    </dependency>

  • (可选)在 application.yml 中,添加业务相关的配置项

后端项目

  • 关于 Application
package com.supwisdom.institute.backend.admin.bff;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import com.supwisdom.infras.security.configure.basic.EnableInfrasBasicApi;
import com.supwisdom.infras.security.configure.cas.EnableInfrasCasSecurity;
import com.supwisdom.infras.security.configure.jwt.EnableInfrasJWTApi;
import com.supwisdom.institute.backend.common.core.transmit.annotation.EnableSimpleUserTransmit;
import com.supwisdom.institute.backend.common.framework.exception.EnableCustomExceptionHandler;

@SpringBootApplication
@EnableFeignClients

@EnableSimpleUserTransmit
@EnableCustomExceptionHandler

@EnableInfrasCasSecurity

@EnableInfrasBasicApi
@EnableInfrasJWTApi
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
  
  @Bean
  public CorsFilter corsFilter() {
    final CorsConfiguration config = new CorsConfiguration();
    //config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/v2/api-docs", config);
    source.registerCorsConfiguration("/api/**", config);  // 对 /api/** 下的请求,支持 cors 跨域请求,如不需要可以注释

    return new CorsFilter(source);
  }

}

注解 @SpringBootApplication

注解 @EnableFeignClients,支持 FeignClient

注解 @EnableSimpleUserTransmit,从请求中接收 User 信息,通过 Feign 调用外部服务时,传递 User 信息

注解 @EnableCustomExceptionHandler,将异常转换为符合开发规范的 json 数据

注解 @EnableInfrasCasSecurity,支持 cas,在 application.yml 可配置是否启用

注解 @EnableInfrasBasicApi,支持 basic 认证(一般在开发环境启用),在 application.yml 可配置是否启用

注解 @EnableInfrasJWTApi,支持 jwt 认证(一般在生产环境启用),在 application.yml 可配置是否启用

  • 关于接口请求

后端项目中,使用服务接口的方式有 2 种,一是 请求转发,二是 远程调用

请求转发

请求转发的方式,基于 spring-cloud-gateway 实现

  • 在 pom.xml 中,添加依赖配置
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

  • 在 application.yml 中,添加 spring.cloud.gateway.routes 配置
spring:
  cloud:
    gateway:
      routes:
      - id: base-api
        uri: http://localhost:8081
        predicates:
        - Path=/api/base/**
        filters:
        - RewritePath=/api/base/(?<suffix>.*), /$\{suffix}
      - id: biz-api
        uri: http://localhost:8081
        predicates:
        - Path=/api/biz/**
        filters:
        - RewritePath=/api/biz/(?<suffix>.*), /$\{suffix}

疑问?为何不用服务注册与发现

考虑到,后期部署采用 k8s,而 k8s 会提供服务注册与发现的能力,所以此处不再考虑

若存在脱离 k8s 部署的情况,可以考虑采用 nginx 实现服务端负载均衡即可,或其他方案

远程调用

远程调用的方式,基于 spring-cloud-openfeign 实现

  • 在 pom.xml 中,添加依赖配置
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

  • 在 Application 中,添加注解 @EnableFeignClients@EnableSimpleUserTransmit

注解 @EnableFeignClients,表示启用 FeignClients 的配置

注解 @EnableSimpleUserTransmit,表示将 后端的登录用户信息,在请求 服务接口 的过程中,进行传递(加入到请求头 Header 中)

  • 在 application.yml 中,添加 服务接口地址 配置

如:

sw-backend-biz-api.uri: http://localhost:8081
  • 在 remote 包 下,创建 FeignClient 类、FallbackFactory 类
package com.supwisdom.institute.backend.admin.bff.apis.remote.biz;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.alibaba.fastjson.JSONObject;
import com.supwisdom.institute.backend.admin.bff.apis.model.biz.Biz;

@FeignClient(
    name = "biz-biz-remote-feign-client",
    url = "${sw-backend-biz-api.uri}/v1/admin/biz",
    fallbackFactory = BizRemoteFallbackFactory.class
)
public interface BizRemoteFeignClient {
  
  @RequestMapping(method = RequestMethod.GET)
  JSONObject query(
      @RequestParam(name = "loadAll") boolean loadAll,
      @RequestParam(name = "pageIndex") int pageIndex,
      @RequestParam(name = "pageSize") int pageSize
      
  );
  
  @RequestMapping(method = RequestMethod.GET, path = "/{id}")
  JSONObject load(
      @PathVariable(name = "id") String id
  );

  @RequestMapping(method = RequestMethod.POST)
  JSONObject create(
      @RequestBody Biz biz
  );

  @RequestMapping(method = RequestMethod.PUT, path = "/{id}")
  JSONObject update(
      @PathVariable(name = "id") String id,
      @RequestBody Biz biz
  );
  
  @RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
  JSONObject delete(
      @PathVariable(name = "id") String id
  );

}

注解 @FeignClienturl 指定后端服务的地址,fallbackFactory 指定熔断回调处理的工厂类

package com.supwisdom.institute.backend.admin.bff.apis.remote.biz;

import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSONObject;
import com.supwisdom.institute.backend.admin.bff.apis.model.biz.Biz;
import com.supwisdom.institute.backend.admin.bff.remote.FallbackError;

import feign.hystrix.FallbackFactory;

@Component
public class BizRemoteFallbackFactory implements FallbackFactory<BizRemoteFeignClient> {

  @Override
  public BizRemoteFeignClient create(Throwable cause) {
    return new BizRemoteFeignClient() {

      @Override
      public JSONObject query(boolean loadAll, int pageIndex, int pageSize) {
        if (cause != null) {
          cause.printStackTrace();
        }
        return FallbackError.defaultErrorJson(cause);
      }

      @Override
      public JSONObject load(String id) {
        if (cause != null) {
          cause.printStackTrace();
        }
        return FallbackError.defaultErrorJson(cause);
      }

      @Override
      public JSONObject create(Biz biz) {
        if (cause != null) {
          cause.printStackTrace();
        }
        return FallbackError.defaultErrorJson(cause);
      }

      @Override
      public JSONObject update(String id, Biz biz) {
        if (cause != null) {
          cause.printStackTrace();
        }
        return FallbackError.defaultErrorJson(cause);
      }

      @Override
      public JSONObject delete(String id) {
        if (cause != null) {
          cause.printStackTrace();
        }
        return FallbackError.defaultErrorJson(cause);
      }
      
    };
  }

}