# LDP 框架自定义注解使用说明文档

本文档是帮助开发者快速熟悉LDP框架中的自定义注解。

## 一、自动填充注解

### 1.1. @AutoComputed

依赖：base-api。

#### 1.1.1 注解介绍

**@AutoComputed**是一个字段注解，标识**实体类**字段为自动填充的注解，需要配合服务层的 **@AutoService** 使用，拥有四个属性，command、ref、refs、format。注解可以通过command标识字段自动装配的方式，根据字段名查找对应计算方法，或根据command类型查找对应系统计算方法。command默认值为 **ComputedCommand.AUTO** ，以下是内置command每个值的解释：

**AUTO**：根据字段名进行填充。

**MD5**：将字段值进行一次md5加密。

**DATETIME**：当前时间。

**USERID**：当前登录用户。

**UUID**：生成32位随机字符串。

**PINYIN**：将ref字段的拼音填充到此字段中

**REFERENCE**：将refs字段的值通过format规则填充到此字段



#### 1.1.2 使用案例

```java
// 值为AUTO，通过属性名查找计算方式，这里的计算方式就是createId
@AutoComputed
private String createId;

// 值为MD5，将原本的明文做一次md5加密并重新赋值到字段中
@AutoComputed(command = ComputedCommand.MD5)
private String userPassword;

// 值为DATETIME，将当前时间填充到字段中
@AutoComputed(command = ComputedCommand.DATETIME)
private Date createTime;

// 值为USERID，将当前登录用户ID填充到字段中
@AutoComputed(command = ComputedCommand.USERID)
private String updateId;

// 值为UUID，将随机生成的UUID填充到字段中
@AutoComputed(command = ComputedCommand.UUID)
private String uuId;

// 值为PINYIN，ref为userName，将userName字段的中文拼音自动填充到字段中
@AutoComputed(command = ComputedCommand.PINYIN, ref = "userName")
private String sortName;

// 值为REFERENCE,refs是一个数组, 将数组中的字段值按照format的规则填充到字段中
@AutoComputed(command = ComputedCommand.REFERENCE, refs = {"name", "age", "gender"}, format = "%s-%s-%s")
private String referenceValue;
```

#### 1.1.3 自定义填充算法

当command值为 **ComputedCommand.AUTO** 时，会通过字段名查找计算方式，这里介绍一下如何自定义填充算法。自动填充算法分为三种：

- 第一种是有参算法，例如MD5算法需要使用到原字段中的值作为参数：

```java
// 值为MD5，将原本的明文做一次md5加密并重新赋值到字段中
@AutoComputed(command = ComputedCommand.MD5)
private String userPassword;
```

需要实现 **ComputerArgs** 接口，将自动填充算法写到computed中。例如Md5填充算法代码如下：

```java
public class ComputerMd5 implements ComputerArgs {
    @Override
    public String computed(Object plain) {
        //这里进行处理，并返回自动填充内容
        return DigestUtils.md5Hex(plain.toString());
    }
}
```

- 第二种也是有参算法，主要是为了按照format进行自动填充，例如：

```java
// 值为REFERENCE,refs是一个数组, 将数组中的字段值按照format的规则填充到字段中
@AutoComputed(command = ComputedCommand.REFERENCE, refs = {"name", "age", "gender"}, format = "%s-%s-%s")
private String referenceValue;
```

需要实现 **ComputerFormatArgs** 接口，将自动填充算法写到computed中。例如REFERENCE填充算法如下：

```java
public class ComputerReference implements ComputerFormatArgs {
    @Override
    public String computed(Object target, String format) {
        String result = "";
 		//核心代码如下，如果format为空则默认以下划线拼接，否则按fromat格式化
        if (StringUtils.isEmpty(format)) {
            result = StringUtils.joinWith("_", objects);
        } else {
            result = String.format(format, objects);
        }
        return result;
    }
}
```

- 第三种是无参算法，例如DATETIME算法就是一个无参算法，只需要获取当前时间：

```java
// 值为DATETIME，将当前时间填充到字段中
@AutoComputed(command = ComputedCommand.DATETIME)
private Date createTime;
```

无参算法与有参类似，只是没有参数，需要实现 **ComputerNoArgs** 接口

```java
public class ComputerDate implements ComputerNoArgs<Date> {
    @Override
    public Date computed() {
        return new Date();
    }
}
```

- 完成算法编写后，还需要将计算类与字段名对应起来。

在service包中新建computed包（推荐），新建一个类，加上 **@Configuration** 注解，在 **@Bean** 注解中的name属性填上字段名。例如字段createId的填充算法是一个无参算法，方法返回类型为 **ComputerNoArgs** ，如果为有参算法，则应该返回 **ComputerArgs** 或者 **ComputerFormatArgs**。

```java
@Configuration
public class ComputedResolver {

    @Bean(name = "createId")
    public ComputerNoArgs getCreateId() {
        return new ComputerUser();
    }
}
```

当AOP在处理自动填充时，这个字段就会根据上面编辑的算法进行自动填充。

### 1.2. @AutoService

依赖：mcs-common

#### 1.2.1 注解介绍

**@AutoService**是一个方法注解，被 **@AutoService** 注解的方法，都会被AOP拦截，并根据实体类字段中的 **@AutoComputed** 规则对字段自动填充。拥有两个属性 mode、init，mode为填充方式，默认为     **ComputedMode.COMPUTED** ，init为是否初始化，初始化则全部填充，否则只填充updateId、updateTime、sortName。

默认取方法的第一个参数进行填充，支持单个实体，也支持List集合。

#### 1.2.2 使用案例

一般@AutoService用于新增方法，以及更新方法

```java
// 新增方法
@AutoService
public void add(LdpMcsElementInfo elementInfo) {
    genericDaoService.insert(elementInfo);
}

// 更新方法
@AutoService(init = false)
public void update(LdpMcsElementInfo elementInfo) {
    genericDaoService.dynamicUpdate(elementInfo);
}
```

### 1.3. @AutoRelativeComputed

依赖：mcs-common

#### 1.3.1 注解介绍

**@AutoRelativeComputed** 是一个方法注解，对使用JPA配置了关联关系实体类进行自动填充，主要作用于Service层，对新增关联和移除关联方法参数进行自动填充。

@AutoRelativeComputed字段详解：

**mode**：关联类型，目前支持RelativeMode.MTM（多对多）、RelativeMode.OTM（一对多）、RelativeMode.MTO（多对一）。**PS：一对多、多对一暂时不建议使用**

**inverse**：是否为逆向，false为单向绑定，true为双向绑定，默认为false。

**source**：参数源类型。

**target**：参数目标类型。

**joinField**：关联字段。

**inverseField**：逆向关联字段。

**exComputed**：扩展的填充字段。

#### 1.3.2 使用案例

##### 1). 多对多关系

这里举例为用户和角色的多对多关系。

用户表（user）：

| 字段名称  | 类型   | 备注             |
| --------- | ------ | ---------------- |
| id        | String | 用户ID，唯一标识 |
| user_name | String | 用户名           |

角色表（role）：

| 字段名称  | 类型   | 备注             |
| --------- | ------ | ---------------- |
| id        | String | 角色ID，唯一标识 |
| role_name | String | 角色名称         |

用户角色关联表（user_role）：

| 字段名称 | 类型   | 备注         |
| -------- | ------ | ------------ |
| id       | String | ID，唯一标识 |
| user_id  | String | 用户ID       |
| role_id  | String | 角色ID       |

1. 使用JPA配置实体类关联关系

   **User.java**
   
   ```java
   /**
   * 与角色的多对多关联
   */
   @JsonIgnoreProperties("userInfos")//避免互相关联死循环
   @ManyToMany(fetch = FetchType.EAGER)
   @JoinTable(name = "user_role", //中间表的名称
              joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
              inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})
   private Set<Role> roleInfos = new HashSet<>();
   ```
   
   **Role.java**
   
   ```java
   
   @JsonIgnoreProperties("roleInfos")
   @ManyToMany(mappedBy = "roleInfos", fetch = FetchType.EAGER, targetEntity = User.class)
   private Set<User> userInfos = new HashSet<>(0);
   ```
   
2. 在**UserServiceImpl**中添加方法和注解

   ```java
   /**
   * 添加角色关联
   *
   * @param id    用户ID
   * @param list  通过注解将List<Role>转换为List<UserRole>
   * @param param 作为删除条件，避免重复数据
   */
   @AutoRelativeComputed(mode = RelativeMode.MTM, source = Role.class, target = UserRole.class, joinField = "userId", inverseField = "roleId")
   public void addRoleToUser(String id, List list, Map<String, Object> param) {
       LinkedList<TranscationEntity> entityList = new LinkedList<>();
       // 删除重复数据
       entityList.add(new TranscationEntity(TransItemExeType.DELETE_ENTITY, UserRole.class, param));
       // 新增
       entityList.add(new TranscationEntity(TransItemExeType.INSERT_ENTITY, list));
       genericDaoService.executeTrans(entityList);
   }

   
   /**
   * 移除角色关联
   *
   * @param id    用户ID
   * @param list  通过注解将List<Role>转换为List<UserRole>
   * @param param 作为删除条件，避免重复数据
   */
   @AutoRelativeComputed(mode = RelativeMode.MTM, source = Role.class, target = UserRole.class,joinField = "userId", inverseField = "roleId")
   public void removeRoleFromUser(String id, List list, Map<String, Object> param) {
       genericDaoService.deleteList(UserRole.class, param);
   }
   ```
   

#### 1.3.3 扩展填充算法

 **@AutoRelativeComputed** 注解主要填充中间关联数据，有部分数据和关联无关，但是又必须填充，这种时候就需要 **exComputed** 来扩展填充算法，可以通过算法来填充其它字段。例如，岗位和组织多对多关联自动填充， exComputed字段值为**orgPosition** ：

```java
@AutoRelativeComputed(mode = RelativeMode.MTM, source = LdpMcsOrganization.class, target = LdpMcsPositionOrganization.class,joinField = "postId", inverseField = "orgId", exComputed = "orgPosition")
```

编写扩展算法需要实现 **CustomComputer** 接口，并在computed方法中编写自动填充逻辑。computed方法有一个参数，这个参数是注解中target类的对象，可以在方法中拿到对象，并对对象中的其它字段进行填充。这里对manager字段进行填充：

```java
public class ComputerOrgPosition implements CustomComputer {
    @Override
    public void computed(Object target) {
        LdpMcsPositionOrganization positionOrganization = (LdpMcsPositionOrganization) target;
        positionOrganization.setManager(false);
    }
}
```

在有 **@Configuration** 注解类中完成Bean的装配：

```java
@Configuration
public class ExtraComputer {
    /**
     * 关联表其它业务数据填充
     *
     * @return
     */
    @Bean(name = "orgPosition")
    public CustomComputer getUserPositionComputer() {
        return new ComputerOrgPosition();
    }
}
```

## 二 、缓存注解

### 2.1. @ApplicationCacheable

依赖：common-cache

#### 2.1.1 注解介绍

 **@ApplicationCacheable** 注解是方法注解，将注解添加到方法上，会自动将方法结果缓存下来，之后再次调用此接口，会直接从缓存中获取结果。缓存key是自动生成的，生成算法为application:+包名+类名+方法名+:参数长度。其它属性字段与Spring cache 注解 **@Cacheable** 属性字段一致。

#### 2.1.2 使用案例

一般此注解在REST层使用，需要注意的是Application缓存不会过期，会一直存在，如果需要刷新，需要另外写刷新方法，建议在数据变化较小，数据量大的接口使用。

```java
/**
* rest获取用户分页数据
*
* @return
*/
@GetMapping("/rest/page")
@ApplicationCacheable
public RestResult getListByRest() {
    return new RestResult(ResultMsg.SUCCESS, exampleService.getUserListByRest());
}
```

### 2.2. @SessionCacheable

依赖：common-cache

#### 2.2.1 注解介绍

 **@SessionCacheable** 注解是方法注解，用法与 **@ApplicationCacheable** 注解基本一致，不一致的地方有两点：1、SessionCache生命周期是跟随登录的，也就是退出登录后失效； 2、缓存key生成规则不一致，生成算法：token:+包名+类名+方法名+:参数长度。

#### 2.2.2 使用案例

退出登录后，Session缓存失效

```java
/**
* jdbc 获取用户list
*
* @param paramMap
* @return
* @throws Exception
*/
@GetMapping("/jdbc/list")
@SessionCacheable
public RestResult getListByJdbc(@RequestParamMap(typeCovertMapping = ExampleUserInfo.class) Map<String, Object> paramMap) throws Exception {
    List<ExampleUserInfo> userInfoList = exampleService.getUserListByJDBC(StringUtils.EMPTY, paramMap);
    return new RestResult(ResultMsg.SUCCESS, userInfoList);
}
```

## 三、日志注解

### 3.1. @AvoidRepeatableSubmit

#### 3.1.1 注解介绍

此注解配合定时任务进行使用，在被需要定时任务调用的服务接口上，添加此注解防止接口由于网络波动或其他原因重复调用。

#### 3.2.2 使用案例
```java 
    @PostMapping(value = "/testExampleJob")
    @AvoidRepeatableSubmit
    public RestResult testJob(@RequestParam String taskJobId, @RequestBody ExampleUserInfo userInfo){
        exampleService.add(userInfo);
        return new RestResult(ResultMsg.SUCCESS,"");
    }
```
**使用时仅需要在被定时任务调用的接口上添加@AvoidRepeatableSubmit即可**

### 3.2. @SysAuditLog

#### 3.2.1 注解介绍
@SysAuditLog 是一个方法注解，需要放在controller方法上。 会拦截包裹的方法，会根据登陆信息自动获取当前用户。此注解进行被请求方法拦截，获取请求地址、请求参数、请求内容、请求方法（GET OR POST）、客户端浏览器信息。
 根据请求状态，判断是异常请求还是正常请求。

#### 3.2.2 使用案例

```java 
   
    /**
     * 用户列表（非分页）
     *
     * @param paramMap 字段条件
     * @return
     */
    @GetMapping("/get/list")
    @SysAuditLog(moduleKey = "example",subKey = "test")
    public RestResult findList(@RequestParamMap(typeCovertMapping = ExampleUserInfo.class) Map<String, Object> paramMap) throws Exception {
        List<ExampleUserInfo> userInfoList = exampleService.findList(paramMap);
        return new RestResult(ResultMsg.SUCCESS, userInfoList);
    }
    
```

  **使用时需传递  moduleKey(模块名称,如果是新模块需提前建立表，以ldp_audit_log_  开头+ modulekey 录入的值结尾的表 如ldp_audit_log_example , 表结构可参考 ldp_audit_log_sys), subKey 子模块名称最终会保存在日志中作为二级子模块**;

### 3.3. @SysJobLog

#### 3.3.1 注解介绍

由于系统架构为微服务，服务与服务之间解耦合。因此定时任务设计之初，考虑为定时接口任务调用。  
定时任务执行接口调用， 在被调用接口引用的Service具体业务方法添加此@SysJobLog 注解。 
注解包含两大功能，1 异步执行，防止接口执行时间过长，否则定时任务调用机器一直连接被占用； 2 更新定时任务执行情况。 
定时任务执行流程：
   1 定时任务调度服务启动后，满足定时条件自动执行任务调度。 根据后台配置好的服务名称、接口信息调用其他服务接口。
   2 被调用接口，必须添加@RequestParam String taskJobId 作为接收参数，此参数后续作为更新任务执行状态使用。由于在业务方法上添加@SysJobLog， 业务方法与Controller不在同一个线程中，无法
   获取上一个线程中的信息。因此具体业务Service也必须添加名称为String taskJobId 的参数，后续会自动获取参数的值进行更新任务执行情况。
   3 被调用接口也必须添加 @AvoidRepeatableSubmit 防止重复执行




#### 3.3.2 使用案例


Controller 

```java

    @PostMapping(value = "/testExampleJob")
    @AvoidRepeatableSubmit
    public RestResult testJob(@RequestBody ExampleUserInfo userInfo, @RequestParam String taskJobId){
	        exampleService.doJob (userInfo, taskJobId);
        return new RestResult(ResultMsg.SUCCESS,"");
    }
    
```

具体Service

```java

    @Override
    @AutoService
    @SysJobLog
    public void doJob(ExampleUserInfo exampleUserInfo, String taskJobId) {
        genericDaoService.insert(exampleUserInfo);
    }
    
```

**具体业务方法上添加@SysJobLog 进行异步任务及String taskJobId 参数，名称需要保持一致，系统会自动进行更新执行状态**``

## 四、数据库查询注解

### 4.1. @QueryBind

依赖：common-query

#### 4.1.1 注解介绍

@QueryBind是一个方法注解，将 **xxxxservice-query.xml** 中的参数SQL语句绑定到方法中的第一个参数中。需要先在 **xxxxservice-query.xml** 中将ref属性指向绑定的xxxServiceImpl类中。

#### 4.1.2 使用案例

**Service层代码：**

```java
/**
* jdbc查询， QueryBind所对应的sql语句在resource/query/exampleservice中
* 调用时 getUserListByJDBC(StringUtils.EMPTY); 注解会将sql语句自动注入到参数中
*
* @param sql
* @return
*/
@QueryBind("examplelist")
@Override
public List<ExampleUserInfo> getUserListByJDBC(String sql, Map<String, Object> param) {
    List<ExampleUserInfo> exampleUserInfos = jdbcDaoService.queryForList(sql, ExampleUserInfo.class, param);
    return exampleUserInfos;
}
```

**REST层代码：**调用时 **getUserListByJDBC(StringUtils.EMPTY)**; 注解会将sql语句自动注入到参数中

```java
Map<String, Object> param = new HashMap<>(1);
param.put("userType", 0);
List<ExampleUserInfo> exampleUserInfos = exampleService.getUserListByJDBC(StringUtils.EMPTY, param);
```

### 4.2. @QueryContextComponent与 @QueryParams

依赖：common-query

#### 4.2.1 注解介绍

**@QueryContextComponent**是一个类注解， **@QueryParams** 是一个方法注解。除了使用 **@QueryBind** 注解注入sql到参数中，还可以使用 **@QueryContextComponent** 结合 **@QueryParams** 注解将xml中的sql全部注入到一个Map对象中。

#### 4.2.2 使用案例

首先在类名上添加 **@QueryContextComponent** 注解，定义一个sqlMap字段，加上 **@QueryParams** 注解，由于 **@QueryParams** 是一个方法注解，所以需要在字段上加上 **@Setter** ，这样会自动生成set方法，注解也会添加到自动生成的方法上

```java
@QueryContextComponent
public class UserBizService   {

    // 由于@QueryParams是一个方法注解，所以此处需要加上@Setter注解
    @Setter
    @QueryParams
    public Map<String,String> sqlMap;
    
    public void getSqlKey() {
        sqlMap.forEach((k,v)->{
            log.info("id:{} {} {} ",k,String.format("%n"),v);
        });
    }
}
```

## 五、REST层参数解析注解

### 5.1. @RequestParamMap

依赖：mcs-common

#### 5.1.1 注解介绍

**@RequestParamMap**是一个参数注解，主要用于在REST层接收GET参数时保持参数类型与实体类一致，可以直接将GET参数接收到的map作为查询条件，另外也是为了将分页参数从查询参数中区分开来。

#### 5.1.2 使用案例

直接在Map参数前使用**@RequestParamMap(typeCovertMapping = XXXX.class)**，这里的XXX指实体类类型。

```java
/**
* 用户列表（非分页）
*
* @param paramMap 字段条件
* @return
*/
@GetMapping("/get/list")
public List<LdpMcsUserInfo> findList(@RequestParamMap(typeCovertMapping = LdpMcsUserInfo.class) Map<String, Object> paramMap) throws Exception {
    return userService.findList(paramMap);
}
```

### 5.2 @RequestParamPage

依赖：mcs-common

#### 5.2.1 注解介绍

**@RequestParamPage**是一个参数注解，主要用于GET请求时接收分页参数。

#### 5.2.2 使用案例

使用封装好的分页类 **Pagination** 来接收分页参数，在分页参数前添加 **@RequestParamPage** 即可。这里是将分页参数和查询参数进行拆分的案例：

```java
/**
* 用户列表(分页)
*
* @param paramPage 分页参数为pageIndex,pageSize
* @param paramMap  字段条件
* @return
*/
@GetMapping("/get/page")
public Pagination findPage(@RequestParamPage Pagination paramPage,
                           @RequestParamMap(typeCovertMapping = LdpMcsUserInfo.class) Map<String, Object> paramMap)
    throws Exception {
    return userService.findPage(paramPage, paramMap);
}
```

## 六、Excel导入导出注解

### 6.1. @ExcelImport

依赖：common-biz-annotation

#### 6.1.1 注解介绍

**@ExcelImport** 是一个方法注解，主要用于实现Excel导入逻辑。

#### 6.1.2 使用案例

将注解直接加到导入方法上，会自动构建ExcelOptions参数。

```java
 /**
  * 通过注解导入Excel，注解完成三件事情：
  * 1、上传文件
  * 2、读取Excel数据
  * 3、根据实体类导入到指定的表中
  *
  * @param file
  * @param excelOptions
  */
@ExcelImport(targetClass = ExampleUserInfo.class)
public void importExcelByAnnotation(MultipartFile file, ExcelOptions excelOptions) {
    excelService.importExcel(excelOptions);
}
```

### 6.2. @ExcelExport

依赖：common-biz-annotation

#### 6.2.1 注解介绍

**@ExcelExport** 是一个方法注解，主要用于实现Excel导入逻辑。

#### 6.2.2 使用案例

将注解直接加到导出方法上，会自动构建ExcelOptions参数。

```java
/**
 * 通过注解导出Excel，注解完成两件事情：
 * 1、读取数据库数据
 * 2、写入到Excel文件中
 *
 * @param outputStream
 * @param excelOptions
 */
@Override
@ExcelExport(sourceClass = ExampleUserInfo.class)
public void exportExcelByAnnotation(OutputStream outputStream, ExcelOptions excelOptions) {
    excelService.exportExcel(excelOptions);
}
```

## 七、模糊查询注解

### 6.1. @HqlQueryFilter

依赖：common-biz-annotation

#### 6.1.1 注解介绍

**@HqlQueryFilter** 是一个字段注解，主要用于标识实体类字段作为列表过滤字段时，数据库查询的匹配方式。目前仅包含精确匹配（QueryFilterType.EQUAL）、模糊匹配（QueryFilterType.ELIKE）

#### 6.1.2 使用案例

在过滤字段上加上注解，以及匹配方式

```java
/**
 * 名称
 */
@HqlQueryFilter(type = QueryFilterType.LIKE)
@Column(name = "name")
private String name;
```

### 6.1. @HqlQueryFilterService

依赖：common-biz-annotation

#### 6.1.1 注解介绍

**@HqlQueryFilterService** 是一个方法注解，根据实体类字段中的 **@HqlQueryFilter** 规则自动构建查询条件数组LinkedList<Condition>。

#### 6.1.2 使用案例

当Rest层调用此方法时，Aop拦截会通过遍历参数Map，并从实体类中获取相应字段及注解，来构建Condition数组。

```java
@Override
@HqlQueryFilterService(target = ExampleUserInfo.class)
public List<ExampleUserInfo> findListByLikeCondition(Map<String, Object> param, LinkedList<Condition> conditionList) {
    return genericDaoService.findByConditions(ExampleUserInfo.class, conditionList);
}
```





