common,提供公用的工具类、框架代码
以 业务领域 为单位,实现 业务领域、业务接口,如:
每个 业务领域项目下,又分为 领域层 domain、接口层 api
业务类库中,尽量以通用服务的方式实现业务
将 业务类库 包装为 微服务,对外提供 RESTful API,以及相关的接口文档的访问、测试
提供 面向UI 的后端接口
对服务接口的转发(建议,避免使用)
对服务接口的聚合、裁剪、适配
提供用户认证,保护接口的访问权限
面对业务需求,设计时,进行领域划分,保证划分的粒度适中
在 领域层 domain,创建 实体 entity、数据传输对象 dto、持久化 repo、业务逻辑 service
在 接口层 api,创建 值对象 vo、接口 api
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
,指定该属性对应的数据库表的字段名
表名、字段名 都采用大写,多个单词用 下划线(_)
隔开
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)
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 为一个数据持久化类
其中 BaseJpaRepository
的 selectPageList
方法,默认返回所有数据,不会处理查询条件、排序,故一般都需要由业务自行重写
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); }
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,目前主要用于接口的请求、响应对象的封装
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 { }
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
的跨域请求
<dependency> <groupId>com.supwisdom.institute</groupId> <artifactId>sw-backend-biz-api</artifactId> </dependency>
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 实现
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
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 实现
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
@EnableFeignClients
,@EnableSimpleUserTransmit
注解 @EnableFeignClients
,表示启用 FeignClients 的配置
注解 @EnableSimpleUserTransmit
,表示将 后端的登录用户信息,在请求 服务接口 的过程中,进行传递(加入到请求头 Header 中)
如:
sw-backend-biz-api.uri: http://localhost:8081
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 ); }
注解 @FeignClient
,url
指定后端服务的地址,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); } }; } }