平台基础

1. 简介

base-pom:所有基础库的底层父类,打包为pom文件,规约使用类库的版本。
base-utils:常用工具类的封装。
base-dao:jpa hibernate依赖管理以及常用功能的封装。所有项目的dao层依赖该库。
base-service-api:服务抽象层的依赖,可通过dubbo对外暴露服务层api给内部子系统。
base-service:服务层的依赖。
base-web-api:对外提供restful接口的web服务依赖。
项目整体分层如图 构建项目结构图

2. 使用基础

  1. java8+
  2. 开发框架 springboot2.0.4,spring5,jersey2,swagger2,hibernate-validate4,jpa,hibernate5。

3. 如何使用

请参考快速开始,使用项目初始化器初始化项目。

4. 安全相关配置

4.1. 防重放攻击

重放攻击(Replay Attacks)又称重播攻击、回放攻击,是指攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的正确性。重放攻击可以由发起者,也可以由拦截并重发该数据的敌方进行。攻击者利用网络监听或者其他方式盗取认证凭据,之后再把它重新发给认证服务器。重放攻击在任何网络通过程中都可能发生,是计算机世界黑客常用的攻击方式之一。

重放攻击的基本原理就是把以前窃听到的数据原封不动地重新发送给接收方。很多时候,网络上传输的数据是加密过的,此时窃听者无法得到数据的准确意义。但如果他知道这些数据的作用,就可以在不知道数据内容的情况下通过再次发送这些数据达到愚弄接收端的目的。例如,有的系统会将鉴别信息进行简单加密后进行传输,这时攻击者虽然无法窃听密码,但他们却可以首先截取加密后的口令然后将其重放,从而利用这种方式进行有效的攻击。再比如,假设网上存款系统中,一条消息表示用户支取了一笔存款,攻击者完全可以多次发送这条消息而偷窃存款。

框架如何开启防重放攻击

服务端 轻松一步搞定:ResourceConfig注册防重放Filter,如图:

配置说明:

api.security.replayAttacks.ignore.client: wechat;Android;IOS  # 英文分号隔开,配置后可忽略这类客户端的防重放验证。
api.security.replayAttacks.ignore.path: file/files/qiniu/upLoadToken;user/owninfo # 英文分号隔开,配置后可忽略访问该路径的防重放验证
api.security.replayAttacks.secret: abcdefj #防重放签名秘钥

客户端 头部放入以下字段:

timestamp: 时间戳

nonc:随机字符串,长度32以上,字符数字组合。

sign: 签名

签名生成算法如下:

MD5("secret=" + {密码,请向服务端索要} + "&timestamp=" + timestamp + "&nonce" + nonce + "&path=" + path)
//说明 path 是访问api后的部分,例如访问http://jpxx.com/api/file/upload,则path=file/upload

4.2. API数据加密

未解决服务端访问敏感数据明文易出现泄漏问题,框架提供自动加密算法。

加密算法,采用DES对称加密,加密字节使用Base64编码,客户端服务端统一使用一个加密秘钥即可。

服务端配置秘钥:

api.security.response.secret: fajdofiafoiaoigjadoifjoiadjfoiaufioaefajdagidajoif #数据加密秘钥,不得小于24位

服务端的自动化实现:

API实现接口IDataEncry,WebApiResponse 返回如下:

response( data,true);// 参数 true表示加密返回

5. 项目Spring容器环境

1.Spring Boot Configuration

BaseConfigration 定义了项目jpa、service、web-api等的扫码包,限制包名,提高项目启动效率。

com.**.domain===>domain

com.**.dao===>repository

com.**.server===>ServiceImpl

com.**.api===>web-api

com.**.schedule===>计划任务

使用示例:

@SpringBootApplication
@Import({BaseConfiguration.class, ServiceValidConfiguration.class})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

2.ApplicationContextHolder 提供了一个Spring容器一个钩子,可以使用该类动态获取Spring容器中的Object,示例如下:

ApplicationContextHolder.context.getBean(UserService.class);

6. 如何使用

6.1. DAO层

1.JPA使用简介

${Domain}Repository

接口,JPA自动实现数据的基本CURD,分页查询等,可继承CrudRepository、PagingAndSortingRepository。

${Domain}RepositoryCustom

自实现的数据查询抽象层。

${Domain}RepositoryImpl

自定义(HQL/SQL)实现数据查询。

2.JPA自动实现

public interface RateDetailRepository extends CrudRepository<RateDetail, Long> {
    List<RateDetail> findByStudentIdAndLessonId(Long studentId, Long lessonId);
    RateDetail findByStudentIdAndLessonIdAndCategoryId(Long studentId, Long lessonId,Long categoryId);
    RateDetail findByStudentIdAndUnitIdAndCategoryId(Long studentId, Long unitId,Long categoryId);

    @Modifying
    @Query("update RateDetail u set u.rateId = ?1 where u.unitId = ?2 and u.studentId = ?3")
    int setRateIdFor(Long rateId, Long unitId, Long studentId);

    List<RateDetail> findByStudentIdAndUnitId(Long studentId, Long unitId);
}
可以使用的关键字
Keyword Sample JPQL snippet
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull findByAgeIsNull … where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1(parameter bound with appended%)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1(parameter bound with prepended%)
Containing findByFirstnameContaining … where x.firstname like ?1(parameter bound wrapped in%)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

3.使用@Query(hql)实现查询

@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);

JPA详细教程:https://docs.spring.io/spring-data/data-jpa/docs/current/reference/html/#repositories.single-repository-behaviour

4.Repository基类

BaseRepository封装了hql查询更新等操作,例如queryForSingle、queryForCount、query(List/Page)、update等; 若不希望使用hql,可使用searchList 和 获取分页searchForPage,使用纯java编码的方式,实现复杂查询。本方法主要针对检索,对于传入的查询参数,如果是null则忽略,不用外部再进行判断。

使用实例代码:

query(Page)

  @Override
    public Page<Book> getPageBook(String title, String bookNo, int pageSize, int pageIndex) {
        StringBuffer ql = new StringBuffer("from Book");
        String pre = " where";

        if (title != null) {
            ql.append(pre);
            ql.append(" title like '%" + title + "%'");
            pre = " and";
        }
        if (bookNo != null) {
            ql.append(pre);
            ql.append(" bookNo like '" + bookNo + "%'");
            pre = " and";
        }

        return query(ql.toString(), new PageRequest(pageIndex, pageSize, new Sort(Sort.Direction.DESC, "createTime")), null);
    }

queryForCount

 @Override
    public List<CourseLesson> getCompleteLessonListInCourses(List<Long> courseIds, LocalDateTime lastTime, int count) {
        String ql = "from CourseLesson where status =:status and courseId in :coursesIds and classTime < :lastTime order by classTime desc";
        Map<String, Object> params = new HashMap<>();
        params.put("status", CourseLesson.Status.PUBLISHED);
        params.put("coursesIds", courseIds);
        params.put("lastTime", lastTime);
        return queryForCount(ql, params,count);
    }

query(List)

  @Override
    public List<Course> findByUsId(Long usId) {
        StringBuffer ql = new StringBuffer("from Course c where c.id in (select r.courseId from UserCourseRelation r where r.usId = " + usId + ")");
        return query(ql.toString(), null);
    }

纯Java代码查询

  @Override
    public List<UserRelation> findByInviterId(Long usId, Integer count, LocalDateTime localDateTime) {
        List<Object> params = new ArrayList<>();
        params.add(new QueryParam("inviterId", usId));
        params.add(new QueryParam("updateTime", localDateTime, QueryParam.Logic.LessThan));
        return searchList(UserRelation.class, params, count, new Sort(Sort.Direction.DESC, "updateTime"));
    }
 @Override
    public Page<ShopUser> searchUsers(Long shopId, Long userId, String name, String mobile, Pageable pageable) {
        List<QueryParam> params = new ArrayList<>();
        params.add(new QueryParam("userId", userId, QueryParam.Logic.Equal));
        params.add(new QueryParam("shopId", shopId, QueryParam.Logic.Equal));
        params.add(new QueryParam("name", name, QueryParam.Logic.Contain));
        params.add(new QueryParam("mobile", mobile, QueryParam.Logic.StartWith));
        return searchForPage(ShopUser.class, params, pageable);
    }

复杂查询

 public Page<Activity> searchPage(Long shopId, Activity.ActivityStatus status, String title, ActivityType activityType, Pageable pageable) {
        List queryParams = new ArrayList<>();
        queryParams.add(new QueryParam("shopId", shopId));
        queryParams.add(new QueryParam("title", title, QueryParam.Logic.Contain));
        queryParams.add(new QueryParam("type", activityType));
        queryParams.add(new QueryParam("deleted", false));
        if (status != null) {
            switch (status) {
                case Doing:
                    List<QueryParam> innerParams = new ArrayList<>();
                    innerParams.add(new QueryParam("startTime", LocalDateTime.now(), QueryParam.Logic.LessThan));
                    innerParams.add(new QueryParam("endTime", LocalDateTime.now(), QueryParam.Logic.GreaterThan));
                    innerParams.add(new QueryParam("forever", true, QueryParam.Logic.Equal, QueryParam.LinkMode.Or));
                    queryParams.add(innerParams);
                    break;
                case Expired:
                    queryParams.add(new QueryParam("endTime", LocalDateTime.now(), QueryParam.Logic.LessThan));
                    break;
                case UnStarted:
                    queryParams.add(new QueryParam("startTime", LocalDateTime.now(), QueryParam.Logic.GreaterThan));
                    break;
            }
        }
        Page<Activity> activities = repository.searchForPage(Activity.class, queryParams, pageable).map(activity -> addActivityStatus(activity));
        return activities;
    }

queryParams出入List<QueryParam>相当于加上了一个()。

连表查询请定义查询视图,直接返回视图对象,或在ServiceImpl实现直接返回DTO对象。

5.domain基类

domain基类封装了创建数据库表需要默认携带的审计字段createTime&updateTime、乐观锁、自增ID等。

AuditEntity: 封装createTime&udpateTime

AuditGeneratedIDEntity: 封装createTime&udpateTime&自增ID

GeneratedIDEntity: 自增ID

LockableEntity: 乐观锁字段

6.2. Service API层

1.业务异常

    private int errorCode;
    private String errorMsg;
    private Throwable cause;

业务层业务异常,统一抛出ServiceException。Web层会统一处理封装异常给客户端,客户端显示errorMsg给用户。

关于errorCode的处理,建议整个系统统一编码,预分配错误代码区间。

cause引起的异常对象。

2.自动化验证

需要自动校验的服务实现类或者某方法,使用注解 @Valid。在ServiceImpl或者抽象接口使用hibernate-validation注解,若接口中传递参数是DTO,则DTO字段也可使用注解自动完成校验。

请注意,@Valid,请使用 com.jpxx.base.service.api.validation.Valid.

hibernate-validation相关知识,请查阅相关文档。

例如:

@Service
@Valid
public class UserServiceImpl extends BaseServiceImpl<User, UserDto, Long> implements UserService {


    @Override
    public String login(@NotNull String username,@NotNull  String passwordMD5, @NotNull String clientId) {

        User user = repository.findByUsernameAndPassword(username, passwordMD5);
        if (user == null) throw new ServiceException((ErrorCode.UNKNOW_USER), "账号或密码不正确");
        if (user.getState() == 1) throw new ServiceException(ErrorCode.USER_LOCKED, "用户已锁定");
   /*     if (!user.getUserRole().equals(clientID.getUserRole()))
            throw new AppBusinessException(CommonErrorCode.FORBIDDEN, "非该系统用户不能登陆,请检查用户");*/

        AuthToken.AuthTokenPK tokenPK = new AuthToken.AuthTokenPK(clientId, user.getId());
        Optional<AuthToken> optionalToken = authTokenRepository.findById(tokenPK);
        AuthToken token = null;
        if (optionalToken.isPresent()) {
            token = optionalToken.get();
        }
        if (token == null) {
            token = new AuthToken();
            token.setTokenPK(tokenPK);
        }
        JwtSubject subject = new JwtSubject(user.getId(), user.getUserRole(), clientId);
        //long expire = clientId.equals("web") ? webExpireTime : appExpireTime;
        String tokenStr = JwtUtils.jwtToken(subject, webExpireTime);
        token.setJwtToken(tokenStr);

        authTokenRepository.save(token);
        return tokenStr;
    }


}
  1. Service抽象层Base接口

BaseService是所有业务抽象层接口的Base接口。内部封装了对Domain的CRUD基本操作。

public interface BaseService<DTO, ID extends Serializable> {

    DTO add(@NotNull DTO dto);

    DTO update(@NotNull ID id, @NotNull DTO dto);

    DTO get(@NotNull ID id);

    List<DTO> get(@NotEmpty List<ID> ids);

    List<DTO> getAll(Sort sort);

    List<DTO> getAll();

    void delete(@NotNull ID id);

    Page<DTO> getAll(@NotNull Pageable pageable);
}

6.3. Sevice 实现层

服务实现层的基类,功能如下:

  1. 增删改查功能。
  2. DTO和Domain的默认转换,如果需要自定义转换,请重写dto2D&d2DTO两个方法。
  3. List<Domain>转 List<DTO> & Page<Domain>转Page<DTO>

代码如下

public abstract class BaseServiceImpl<D, DTO, ID extends Serializable> implements BaseService<DTO, ID> {

    @Override
    public DTO add(DTO dto) {
        D domain = toD(dto);
        return toDTO(repository().save(domain));
    }

    @Override
    public DTO update(ID id, DTO dto) {
        Optional<D> model = repository().findById(id);
        if (!model.isPresent()) throw new ServiceException(404, "更新对象ID不存在");
        D domain = model.get();
        BeanUtils.copyProperties(dto, domain);
        return toDTO(repository().save(domain));
    }

    @Override
    public DTO get(ID id) {
        D domain = repository().findById(id).orElse(null);
        if (domain == null) return null;
        else return toDTO(domain);
    }

    @Override
    public List<DTO> get(List<ID> ids) {
        return (List<DTO>) repository().findAllById(ids);
    }

    @Override
    public List<DTO> getAll(Sort sort) {
        return (List<DTO>) repository().findAll(sort);
    }

    @Override
    public List<DTO> getAll() {
        return (List<DTO>) repository().findAll();
    }

    @Override
    public Page<DTO> getAll(Pageable pageable) {
        Page<D> dPage = repository().findAll(pageable);
        return toDTOPage(dPage);
    }

    @Override
    public void delete(ID id) {
        repository().deleteById(id);
    }


    public Page<DTO> toDTOPage(Page<D> domainPage) {
        List<DTO> dtoList = toDTOList(domainPage.getContent());
        return new PageImpl<DTO>(dtoList, domainPage.getPageable(), domainPage.getTotalElements());
    }

    public List<DTO> toDTOList(List<D> modelList) {
        List<DTO> dtoList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(modelList)) {
            modelList.forEach(model -> {
                DTO dto = toDTO(model);
                dtoList.add(dto);
            });
        }
        return dtoList;
    }

    public DTO toDTO(D domain) {
        DTO dto = newDTO();
        d2DTO(domain, dto);
        return dto;
    }

    public D toD(DTO dto) {
        D domain = newDomain();
        dto2D(dto, domain);
        return domain;
    }

    public void d2DTO(D domain, DTO dto) {
        if (domain == null || dto == null) throw new ServiceException(500, "d2DTO null pointer exception.");
        BeanUtils.copyProperties(domain, dto);
    }

    public void dto2D(DTO dto, D domain) {
        if (domain == null || dto == null) throw new ServiceException(500, "dto2D null pointer exception.");
        BeanUtils.copyProperties(dto, domain);
    }

    public abstract D newDomain();

    public abstract DTO newDTO();

    public abstract PagingAndSortingRepository<D, ID> repository();

}

连表查询

连表查询使用hql: select new ***Dto(arg1,arg2) from ... 可以直接查询出dto对象。

6.4. Web API 层

  1. Response封装

接口层返回数据统一封装成WebResponse对象,对象包含一下字段:

errorCode:错误代码,0表示正常。

errorMsg:错误提示信息。

data:返回数据。

可以在Web Api层实现接口IResponse,使用方法 response(T data)自动封装,使用done()返回执行成功。

使用示例:

 @DELETE
    @ApiModelProperty(value = "删除一个菜单")
    @Path("{id}")
    @Authorization(permission = {"menu:delete"})
    public WebApiResponse deleteMenu(@PathParam("id") Long id, @Context SecurityContext context) {
        menuService.delMenus(id);
        return done();
    }
 @GET
    @Path("{id}")
    @ApiModelProperty(value = "获取一个菜单详情")
    @Authorization(permission = "menu:read")
    public WebApiResponse<MenuDto> getOneMenu(@PathParam("id") Long id) {
        return response(menuService.get(id));
    }

2.统一异常处理

web层、服务层、dao层抛出异常,都会交给统一处理类进行处理,封装结果返回给客户端。如果出现500异常,打印日志到指定文件,交由运维处理。

3.自定义序列化

日期时间: yyyy-MM-dd HH:mm:ss (Date/LocalDateTime)

日期: yyyy-MM-dd(LocalDate)

Null序列化:默认不携带该字段

Empty(List size=0序列化):默认:[]

4.跨域配置

前端地址放在不同域下的时候,必须打开跨域才能正常访问,打开方式,加入CorsConfguration配置即可。

例如:

@SpringBootApplication
@EnableScheduling
@Import({BaseConfiguration.class, CorsConfguration.class, AuthConfiguration.class, ServiceValidConfiguration.class})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

5.广义接口权限(分类)

系统针对用户接口,做大分类。例如一个滴滴打车平台,用户分类:司机、乘客、管理员。

为了限制每一类用户只能访问该用户类别的接口,每一个用户会有一个userRole标识(详见Auth库)该用户的分类,接口类(或方法)上使用@RoleAllowed(userRole)注解标注接口分类,系统自动实现该接口的用户类别坚定,坚定失败返回401错误。

6.客户端标识相关配置 clientId表示前端项目的唯一标识,也是每次接口访问必须携带在header中的一部分。 auth.clientUserRoles: 该配置项是配置客户端允许登陆的用户。 格式: {clientKey}:{userRole1|userRole2} ,例如:SWAGGER_UI:admin|user;user-wechat:user ,clientKey userRole禁止出现 符号 ; : 不区分大小写 auth.clientTokenExpired: 客户端token的过期时间配置,默认为过期时间为十年。格式:{clientKey}:{数字}{单位: yMdHms} clientKey userRole禁止出现 符号 ; :,例如:admin:10m;user-wechat:10y,单位: 秒(s/S);分钟(m),小时(h/H),天(d/D),月(M),年(Y/y)。

7.其他配置 auth.tokenVerify: true/false 是否验证统一clientId重新登陆,token时间过期,主要用于兼容老系统(0.0.1版本)。 auth.permission: true/false 是否开启权限验证。 auth.tokenVerifyIgnoreClient: user;admin token验证忽略的客户端标识。

7. 强制规范

  1. maven groupId 命名规范 com.jpxx.${projectName} 小写字母,中划线分割
  2. maven artifactId 命名规范 ${moduleName}-[dao/web-api/service/service-api] 小写字母,中划线分割
  3. java package 命名: com.jpxx.${projectName}.${moduleName}.[dao/web.api/service/service.api] 。为保证Spring容器可以顺利扫描domain/dao/service/web.api,包名必须以 dao/domain/service/api结尾。
  4. 按照<<阿里巴巴Java开发手册>>

8. 特别注意

为避免冲突,底层已实现所有基础依赖(base/auth/com组件)版本统一管理,项目无需指定版本号

9. 技术栈

使用该平台,请事先了解相关技术基础,本文档不对技术基础作深入阐述。

源码地址:http://sources.jpsycn.com/dev-plat/java-server/base.git

版权归河南金鹏信息技术股份有限公司所有,仅用于技术交流,禁止用于商业目的 all right reserved,powered by Gitbook该文件修订时间: 2020-03-21 17:16:57

results matching ""

    No results matching ""