# 后端手册

# 安装开发环境-windows

  1. 安装 jdk1.8 https://blog.csdn.net/weixin_42109012/article/details/94388518

  2. 安装 maven https://jingyan.baidu.com/article/2f9b480ddc1c5d41cb6cc217.html

  3. 安装 idea https://blog.csdn.net/weixin_43184774/article/details/100578786

  4. 安装 mysql https://baijiahao.baidu.com/s?id=1630347658327095638&wfr=spider&for=pc

  5. idea 安装 lombokhttps://www.ydyno.com/archives/1147.html

  6. idea 安装 MappingSearch 插件 https://gitee.com/quella01/MappingSearch

下载 MappingSearch.jar 包,可以直接进群,群文件也有提供。

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/mappingsearch.png

  1. idea 添加 maven 骨架

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/eightroes-webapp-archetype.png

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/eightroes-plugin-archetype.png

# 运行项目

  1. 使用 eightroes-webapp-archetype 创建项目

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/eightroes-demo-create.png

第一次生成可能需要一点时间,生成后的目录如下:

https://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/demo-wepapp.png

  1. 到 application.yml 文件中修改数据库链接,sql 文件在 resource 的 sql 文件夹中,可能前面的版本没有这个文件夹,没有的话可以进群,群文件有提供所有版本的 sql 文件,或者直接去源码工程下载。

  2. 运行 webapp 模块 WebappStartApplication.java 启动类即可。 生成的 wepapp 模块,已经依赖了基础平台插件模块,无需再手动依赖。后台启动成功后,你就可以直接启动前端访问了!

TIP

后面我们会提供大量插件,方便大家完成快速集成开发,也欢迎各位大佬和公司提供插件。

  1. 新建插件模块, File->New->Model 选择 eightroes-plugin-archetype 插件名称请用plugin-开头。

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/create-plugin.png

创建好的目录如下:

https://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/demo-plugin.png

我们推荐插件的包结构如下,如果自己公司有规范也是可以使用自己公司规范,但是包名前缀必须为com.ssrs开头:

- com.ssrs.example
- com.ssrs.example.bl  | 逻辑处理类
- com.ssrs.example.cache | 缓存提供者
- com.ssrs.example.code | 代码项
- com.ssrs.example.config | 配置项
- com.ssrs.example.controller | 前端控制器
- com.ssrs.example.extend | 扩展服务
- com.ssrs.example.extend.item | 扩展项
- com.ssrs.example.mapper | mapper文件
- com.ssrs.example.model | 实体类
- com.ssrs.example.model.entity | 数据库映射实体
- com.ssrs.example.model.param | 前端请求参数实体
- com.ssrs.example.model.query | sql查询映射实体
- com.ssrs.example.point | 扩展点
- com.ssrs.example.point.action | 扩展行为
- com.ssrs.example.priv | 菜单权限类
- com.ssrs.example.service | 业务类接口
- com.ssrs.example.service.impl | 业务接口实现类
- com.ssrs.example.service.task | 定时任务
- com.ssrs.example.service.util | 工具类

- com.ssrs.example.ExamplePlugin.java 插件类

然后 webapp 模块添加一下插件模块的依赖,然后启动项目就会加载插件相关内容

<dependency>
    <groupId>top.ssrsdev</groupId>
    <artifactId>plugin-example</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

# 权限控制

我们提供了一个自定义的 shiro 注解@Priv用于接口权限控制,我们推荐使用权限标识来做接口权限控制,并不推荐基于角色来控制,@priv注解正是如此。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Priv {
    boolean login() default true; // 是否需要登录
    String[] value() default {}; // 权限标识
}

新建一个接口 controller 来测试一下,注意继承BaseController

@RestController
@RequestMapping("/api/demo")
public class DemoController extends BaseController {

    // 不需要登陆
    @Priv(login = false)
    @GetMapping("test1")
    public ApiResponses<String> test1() {
        return success("不需要登陆");
    }


    // 需要登陆
    @Priv
    @GetMapping("test2")
    public ApiResponses<String> test2() {
        return success("需要登陆");
    }

    // 需要权限
    @Priv("DemoPriv.Save")
    @GetMapping("test3")
    public ApiResponses<String> test3() {
        return success("需要权限");
    }
}

一般我们都需要将菜单权限返回给前端,我们提供了一个菜单权限扩展服务MenuPrivService,你只需要提供一个菜单扩展项目即可,新建一个菜单权限项,然后加入到resources/plugin/的插件的配置文件扩展项中。

public class DemoPriv extends AbstractMenuPriv {

    public static final String MenuID = "DemoPriv";
    public static final String Save = MenuID + ".Save";

    public DemoPriv() {
        super(MenuID, "demo管理", null);
        addItem(Save, "保存");
    }
}

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/demopriv.png

然后修改 test3 接口

// 需要权限
    @Priv(DemoPriv.Save)
    @GetMapping("test3")
    public ApiResponses<String> test3() {
        return success("需要权限");
    }

创建完后启动,分别访问这三个接口

test1返回: {"status":1,"message":"不需要登陆","data":null}
test2返回:{"status":401,"error":"UNAUTHORIZED","message":"请先进行认证","exception":null,"time":"2020-06-19 20:36:50"}
test3未登录返回:{"status":401,"error":"UNAUTHORIZED","message":"请先进行认证","exception":null,"time":"2020-06-19 20:37:17"}
test3登录返回:{"status":403,"error":"FORBIDDEN","message":"无权查看","exception":"org.apache.shiro.authz.UnauthorizedException: Subject does not have permission [Demo.Add]","time":"2020-06-19 20:42:54"}

如果你的项目必须要用角色控制,我们也提供了角色扩展点com.ssrs.framework.point.AddUserRolesPoint你可以定义扩展行为来实现角色控制,但是注解就不能使用@Priv必须使用 shiro 的注解 。

# 系统缓存

EightRoes 对缓存做了适配,使用的是 Springboot 的 CacheManager 接口来实现缓存切换,我们提供了一个缓存提供者扩展服务CacheDataProvider和系统缓存管理器FrameworkCacheManager,我们推荐你定义缓存提供者扩展项来做缓存,这样当你的项目切换到不同的缓存时,只要修改配置文件即可,无需修改代码,当然,如果项目确定使用某种缓存,你也可以不这样做。

切换系统缓存需要哪些步骤,切换缓存步骤其实跟添加缓存是一样的

  1. 添加缓存的依赖
    <dependency>-->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
  1. 注入缓存 CacheManager
@Configuration
@ConditionalOnProperty(value = "spring.cache.type", havingValue = "redis")
public class RedisCacheConfig extends CachingConfigurerSupport {

    @Resource
    private LettuceConnectionFactory lettuceConnectionFactory;


    /**
     * 配置CacheManager
     *
     * @return
     */
    @Bean
    public CacheManager cacheManager() {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        // 解决jackson2无法反序列化LocalDateTime的问题
        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        om.registerModule(new JavaTimeModule());
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题)
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();

        RedisCacheManager cacheManager = RedisCacheManager.builder(lettuceConnectionFactory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }


    /**
     * RedisTemplate配置
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        // 设置序列化
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
                Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        // 解决jackson2无法反序列化LocalDateTime的问题
        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        om.registerModule(new JavaTimeModule());
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        RedisSerializer<?> stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);// key序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value序列化
        redisTemplate.setHashKeySerializer(stringSerializer);// Hash key序列化
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// Hash value序列化
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}
  1. 修改 application.yaml 配置文件的缓存类型
spring:
  cache:
    type: redis
    redis:
      cache-null-values: true
      use-key-prefix: true
      time-to-live: 20s
  redis:
    password:
    database: 0
    port: 6379
    host: 127.0.0.1
    lettuce:
      pool:
        max-idle: 8
        min-idle: 0
        max-active: 8
        max-wait: -1ms
    timeout: 10000ms

使用建议

缓存的出现加快了数据查询的速度,同时增加了维护成本,建议使用在高频读低频写的数据上。

使用不当可能会出现数据不一致的问题,请谨慎使用。

# 定时任务

定时任务底层就是使用的 Hutool 的定时任务模块,我们并没有额外使用外部的依赖,如Quartz,因为 Quartz 的包太重了,我们也用不到那么复杂的功能。Hutool 的定时任务已经完全满足我们的需求,我们在其基础上封装成了系统扩展服务,也支持动态管理。

EightRoes 创建定时任务也非常简单,创建一个定时任务扩展项后,加入到插件配置即可。

  1. 新建定时任务扩展项
@Slf4j
public class TestTask1 extends SystemTask {

    @Override
    public void execute() {
        // 执行片段
        log.info("---------------测试定时任务1---------------");
    }

    @Override
    public String getDefaultCronExpression() {
        return "0/10 * * * * ?"; // cron 表达式
    }

    @Override
    public String getExtendItemID() {
        return "com.ssrs.platform.task.TestTask1"; // 任务id,使用全限定名即可
    }

    @Override
    public String getExtendItemName() {
        return "测试定时任务1"; // 任务描述
    }
}
  1. 加入配置文件

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/task1.png

  1. 重启后刷新页面

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/web-task.png

# 数据字典

一些固定的选择或配置,如性别有男女,我们就可以使用数据字典来定义它,我们提供了一个扩展服务CodeService来注册数据字典,并且加入到了缓存中。

  1. 新建一个数据字典
public class GenderType extends FixedCodeType {
    public static final String CODETYPE = "GenderType";
    public static final String F = "F"; // 男
    public static final String M = "m"; // 女

    public GenderType() {
        super(CODETYPE, "性别", false, false);
        addFixedItem(F, "男", null);
        addFixedItem(M, "女", null);
    }
}
  1. 加入到插件配置中。

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/code.png

  1. 重启后刷新页面

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/codepage.png

# 配置管理

EightRoes 加入了动态配置管理功能,你可以将项目中的一些配置,使用此功能来支持动态修改,我们也将配置值加入到了缓存中。 eg:系统管理员的配置

我们支持大部分的表单类型配置,如 radio,checkbox,select 等等,具体类型请看代码ControlType

  1. 添加一个配置服务扩展项类
/**
 * 系统管理员的用户名
 */
public class AdminUserName extends FixedConfigItem {
    public static final String ID = "Platform.AdminUserName";

    public AdminUserName() {
        super(ID, DataType.ShortText, ControlType.Text, "系统管理员的用户名");
    }


    public static String getValue() {
        String v = Config.getValue(ID);
        if (StrUtil.isEmpty(v)) {
            v = "admin";
        }
        return v;
    }

}

直接使用:AdminUserName.getValue() 获取值

  1. 将扩展项加入到插件配置中。

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/config.png

  1. 页面配置

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/web-config.png

# 异常处理

EightRoes 针对系统异常返回信息也进行了处理,方便前端对异常进行统一拦截处理。

# 异常实体

@Getter
@ToString
@Builder
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
public class ErrorCode {

    /**
     * 错误
     */
    private String error;
    /**
     * http状态码
     */
    private int httpCode;
    /**
     * 错误消息
     */
    private String message;
}

# 通用异常

TIP

EightRoes 的错误信息分为了两种,一种是正常的错误信息,如手机号码验证不对。一种是异常错误信息,如删除用户的时候是根据 ID 删除的,可判断 ID 是否存在,抛出异常错误信息。

对于异常错误你只需要抛出此异常,系统会自动转换相关信息,我们推荐你使用一个枚举来装换一下。可以参考 ErrorCodeEnum,如果你不想显示的抛出异常,可以使用 ResponseUtils.sendFail()方法来返回错误信息。

public class ApiException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    /**
     * 错误码
     */
    private final ErrorCode errorCode;

    public ApiException(ErrorCodeEnum errorCodeEnum) {
        super(errorCodeEnum.message());
        this.errorCode = errorCodeEnum.convert();
    }

    public ApiException(ErrorCode errorCode) {
        super(errorCode.getError());
        this.errorCode = errorCode;

    }

    public ErrorCode getErrorCode() {
        return errorCode;
    }

}

# 制作一个简单的插件

如果你阅读了上面的文档并且实际操作了,我相信你已经了解 EightRoes,接下来我们制作一个简单的插件来了解一下什么是 EightRoes 的插件机制。

# 创建工程

使用 maven 骨架创建项目,骨架版本与项目发布版本一致

eightroes-webapp-archetype:用于创建项目父模块和 webapp 模块

eightroes-plugin-archetype:用于创建插件模块

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/eightroes-webapp-archetype.png

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/eightroes-plugin-archetype.png

创建完成后的目录如下所示:

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/simple-plugin.png

插件描述文件 com.ssrs.example.xml:

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/pluginxml.png

id: 插件id,默认使用插件主类全限定名
class: 插件主类全限定名
name:插件名称
author: 插件作者
provider:插件提供商
version:插件版本
description: 插件描述
extend-point:扩展点
extend-action:扩展行为
extend-service:扩展服务
extend-item:扩展项

插件可视化视图:

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/plugindesc.png

插件主类:

package com.ssrs.example;

import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.ssrs.framework.extend.plugin.AbstractPlugin;
import com.ssrs.framework.extend.plugin.PluginException;

/**
 * @author ssrs
 */
public class ExamplePlugin extends AbstractPlugin {
    Log log = LogFactory.get(ExamplePlugin.class);
    @Override
    public void start() throws PluginException {
        log.info("==========启动演示插件===============");
    }

    @Override
    public void stop() throws PluginException {
        log.info("==========停止演示插件===============");
    }
}

启动项目后打印信息:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.4.RELEASE)

2020-06-20 23:00:36.291 INFO  Starting WebappStartApplication on PC201512222000 with PID 22652 (G:\EightRoes\eightroes-demo\webapp-eightroes-demo\target\classes started by Administrator in G:\EightRoes\eightroes-demo) [main]
2020-06-20 23:00:36.297 INFO  No active profile set, falling back to default profiles: default [main]
2020-06-20 23:00:41.570 INFO  Context-RealPath: C:/Users/Administrator/AppData/Local/Temp/undertow-docbase.4936934574327085828.8081 [main]
2020-06-20 23:00:41.625 INFO  Loading plugin:com.ssrs.framework.FrameworkPlugin [main]
2020-06-20 23:00:41.627 INFO  Loading plugin:com.ssrs.platform.PlatformPlugin [main]
2020-06-20 23:00:41.628 INFO  Loading plugin:com.ssrs.example.ExamplePlugin [main]
2020-06-20 23:00:44.269 INFO  HikariPool-1 - Starting... [main]
2020-06-20 23:00:44.598 INFO  HikariPool-1 - Start completed. [main]
2020-06-20 23:00:44.769 INFO   Execute SQL:SELECT code_type,parent_code,code_value,code_name,code_order,icon,memo,create_user,create_time,update_user,update_time FROM sys_code ORDER BY code_order ASC , code_type ASC , parent_code ASC [main]
2020-06-20 23:00:44.972 INFO   Execute SQL:SELECT code,name,value,memo,create_user,create_time,update_user,update_time FROM sys_config [main]
2020-06-20 23:00:44.984 WARN  Get cache data failed: Provider=config,Type=eight-roes-config,Key=eight-roes-Platform.AdminUserName [main]
2020-06-20 23:00:45.064 INFO  ==========启动演示插件=============== [main]
2020-06-20 23:00:45.065 INFO  All plugins started,cost 3493 ms [main]
2020-06-20 23:00:45.082 INFO  ----eight-roes(八玫瑰快速开发框架):  Initialized---- [main]
2020-06-20 23:00:46.839 INFO  Started WebappStartApplication in 13.09 seconds (JVM running for 15.846) [main]

# 新建扩展点

  1. 新建一个扩展点类
public abstract class Test1Point implements IExtendAction {
    public static final String ID = "com.ssrs.example.point.Test1Point";

    // 如果需要有返回值直接用这个方法就行,无需重载
    @Override
    public Object execute(Object[] args) throws ExtendException {
        execute();
        return null;
    }

    public abstract void execute();

}
  1. 添加到插件配置中去。
    <extend-point>
        <id>com.ssrs.example.point.Test1Point</id>
        <class>com.ssrs.example.point.Test1Point</class>
        <description>测试扩展点1</description>
    </extend-point>

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/point-view.png

# 新建扩展行为

  1. 新建一个扩展行为类
public class Test1Action extends Test1Point {
    Log log = LogFactory.get(Test1Action.class);
    @Override
    public void execute() {
        log.info("--------------执行扩展行为Test1Action---------------");
    }

    @Override
    public boolean isUsable() {
        return true;
    }
}

  1. 添加到插件配置中去。
<extend-action>
    <id>com.ssrs.example.point.Test1Point</id>
    <class>com.ssrs.example.point.Test1Point</class>
    <description>测试扩展行为1</description>
    <extend-point>com.ssrs.example.point.Test1Point</extend-point>
</extend-action>
  1. 新建一个测试类,测试扩展点和扩展行为
@SpringBootApplication
public class AppTest {

    public static void main(String[] args) {
        SpringApplication.run(WebappStartApplication.class, args);
        TestAction();
    }
    /**
     * Rigorous Test :-)
     */
    public static void TestAction() {
        StaticLog.info("--------扩展行为执行开始------");
        ExtendManager.invoke(Test1Point.ID, new Object[]{});
        StaticLog.info("--------扩展行为执行完成------");
    }
}

4.打印日志

2020-06-20 23:56:00.266 INFO  ==========启动演示插件=============== [main]
2020-06-20 23:56:00.267 INFO  All plugins started,cost 2881 ms [main]
2020-06-20 23:56:00.278 INFO  ----eight-roes(八玫瑰快速开发框架):  Initialized---- [main]
2020-06-20 23:56:02.100 INFO  Started AppTest in 11.48 seconds (JVM running for 14.06) [main]
2020-06-20 23:56:02.104 INFO  --------扩展行为执行开始------ [main]
2020-06-20 23:56:02.107 INFO  --------------执行扩展行为Test1Action--------------- [main]
2020-06-20 23:56:02.107 INFO  --------扩展行为执行完成------ [main]

经过阅读文档和实操,你应该能明白扩展点与扩展行为的作用了。

为了让一个插件的 JAVA 类运行到指定行数时,可以执行其他插件中的指定的程序逻辑(通常是额外的数据校验和处理逻辑),需要定义扩展点。

扩展点是一个插件配置项,用于声明本插件的一个扩展行为注册入口。程序执行到扩展点所在的行时会查找所有注册到该扩展点的扩展行为,并执行所有的扩展行为指定的类。扩展项也是一个插件配置项,用于声明向哪个扩展点注册扩展行为,并指定该扩展点被调用时执行的类。

扩展点类型于 SWING 中的事件(Event),扩展行为则类似于监听器(Listener)。

# 新建扩展服务

  1. 新建一个接口或抽象类
public interface ITest extends IExtendItem {
    void test();
}

  1. 新建一个扩展服务类
public class TestService extends AbstractExtendService<ITest> {

    public static TestService getInstance() {
        return findInstance(TestService.class);
    }
}
  1. 添加到插件配置中去。
<extend-service>
    <id>com.ssrs.example.extend.TestService</id>
    <class>com.ssrs.example.extend.TestService</class>
    <description>测试扩展服务</description>
    <item-class>com.ssrs.example.extend.ITest</item-class>
</extend-service>

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/service.png

  1. 这样扩展服务就定义好了,接下来将扩展项注入到此服务即可。

# 新建扩展项

1.新建两个 Test 服务扩展项

public class TestItem1 implements ITest {
    Log log = LogFactory.get(TestItem1.class);
    public static final String ID = "com.ssrs.example.extend.item.TestItem1";

    @Override
    public void test() {
        log.info("---------------执行Test扩展项1---------------");
    }

    @Override
    public String getExtendItemID() {
        return ID;
    }

    @Override
    public String getExtendItemName() {
        return "Test扩展项1";
    }
}
public class TestItem2 implements ITest {
    Log log = LogFactory.get(TestItem2.class);
    public static final String ID = "com.ssrs.example.extend.item.TestItem2";

    @Override
    public void test() {
        log.info("---------------执行Test扩展项2---------------");
    }

    @Override
    public String getExtendItemID() {
        return ID;
    }

    @Override
    public String getExtendItemName() {
        return "Test扩展项2";
    }
}
  1. 添加到插件配置中去。
<extend-item>
    <id>com.ssrs.example.extend.item.TestItem1</id>
    <class>com.ssrs.example.extend.item.TestItem1</class>
    <description>Test扩展项1</description>
    <extend-service>com.ssrs.example.extend.TestService</extend-service>
</extend-item>
<extend-item>
    <id>com.ssrs.example.extend.item.TestItem2</id>
    <class>com.ssrs.example.extend.item.TestItem2</class>
    <description>Test扩展项2</description>
    <extend-service>com.ssrs.example.extend.TestService</extend-service>
</extend-item>

http://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/item.png

  1. 新建测试运行
@SpringBootApplication
public class AppTest2 {

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

    public static void TestService() {
        // 获取全部扩展项
        List<ITest> testList = TestService.getInstance().getAll();
        for (ITest iTest : testList) {
            iTest.test();
        }

        // 获取指定扩展项
        ITest test1 = TestService.getInstance().get(TestItem1.ID);
        test1.test();
    }
}
  1. 日志打印
2020-06-21 13:22:00.205 INFO  ==========启动演示插件=============== [main]
2020-06-21 13:22:00.205 INFO  All plugins started,cost 3152 ms [main]
2020-06-21 13:22:00.216 INFO  ----eight-roes(八玫瑰快速开发框架):  Initialized---- [main]
2020-06-21 13:22:02.241 INFO  Started AppTest2 in 11.851 seconds (JVM running for 14.158) [main]
2020-06-21 13:22:02.248 INFO  ---------------执行Test扩展项1--------------- [main]
2020-06-21 13:22:02.248 INFO  ---------------执行Test扩展项2--------------- [main]
2020-06-21 13:22:02.249 INFO  ---------------执行Test扩展项1--------------- [main]

经过制作简单插件这一章节,你应该明白了扩展点,扩展行为,扩展服务,扩展项的作用。EightRoes 正式基于此来实现插件机制。

某一插件实现的功能需要依赖于本插件定义的某一接口的子类的集合时,需要定义扩展服务。扩展服务是一个插件配置项,用于声明本插件的一个扩展项注册入口;扩展项也是一个插件配置项,用于声明向哪个扩展服务注册扩展项。扩展项指定的类必须实现扩展服务指定的接口。

例如: 平台基础插件需要管理所有的短信服务,但是不知道其他插件都实现哪些短信服务,所以需要提供一个扩展服务,其他插件则可以将自己实现的短信服务扩展项注册到此服务,从而通知平台插件可以使用哪些短信服务。

# 接口文档

EightRoes 的接口文档使用的是swagger的增强版knife4j。使用方式与swagger一致,其加入了一下扩展扩展功能,默认访问地址为:http://{ip}:{port}/doc.html

https://eightroes-doc.oss-cn-beijing.aliyuncs.com/img/knife4j.png

# 工具类

# BaseController

针对返回值做了封装处理。注意的是 EightRoes 对 Controller 封装了两种类型的返回值,如果你的项目没有分前后端人员,是前后端一起撸的话,就没必须生成 swagger 接口文档,直接继承 BaseController,返回 ApiResponses 即可。

@Priv(login = false)
@GetMapping
public ApiResponses<String> get() {
    return success("success");
}

如果你的项目需要生成 swagger 接口文档请使用 EightRoes 封装的 SwaggerModel 模块返回并且不需要继承 BaseController SwaggerModel 封装的 model 在com.ssrs.framework.web.swaggermodel包下。

@Priv(login = false)
@GetMapping
public ResultModel<String> get() {
    return ResultModel.success("success");
}

# Current

各线程数据分离工具,可以直接获取当前线程 Request 请求,Responese 响应,User 用户等值。

Current.getUser() // 获取当前用户
Current.getRequest() // 获取当前请求
Current.getRequest().getClientIP() // 获取客户端id
Current.getRequest().getHeaders() // 获取请求头
Current.getRequest().getHttpMethod() // 获取请求方法
Current.getRequest().getPort() // 获取请求端口
Current.getRequest().getQueryString() // 获取请求参数
Current.getRequest().getURL() // 获取请求url
Current.getRequest().getServletRequest() // 获取请求ServletRequest
...
Current.getResponse() // 获取当前响应


// 你还可以使用它存入当前线程值
Current.put("key", "value");
Current.get("key")

# User

获取当前用户信息工具

User.getCurrent() // 获取当前用户信息
User.getUserName() // 获取当前用户名
User.getRealName() // 获取当前户名真实姓名
User.getPrivilege() // 获取当前户名权限
User.isBranchAdministrator() // 获取当前户是否是机构管理员
User.getBranchInnerCode() // 获取当前用户机构编码
User.getValue() // 按key获取指定数据项

# Config

获取系统配置值以及配置管理的值

framework.setting 中配置的值和配置项(动态配置管理)可以直接使用 Config 来获取到值

// 按key获取指定数据项,可以获取配置项(动态配置管理的值)
Config.getValue()

Config.getAppCode() // 获取应用code
Config.getAppCode() // 获取应用名称
Config.getAppVersion() // 获取应用版本
Config.getAppDataPath() // 获取appdata文件路径
Config.getBuildInfo() // 获取构建信息
Config.getApplicationRealPath() // 获取应用真实路径
Config.getContainerInfo() // 获取中间件容器信息
Config.getContainerVersion() // 获取中间件容器版本
Config.getServletContext() // 获取ServletContext
Config.getWebApplicationContext() // 获取WebApplicationContext
Config.getFileSeparator() //  文件名中的路径分隔符
Config.getOSName() // 操作系统名称
Config.getIpAddress() // 返回IP地址
.....
更多信息请自行查看

# AuthCodeUtil

生成验证码工具类

# ExpiringCacheSet 和 ExpiringSet

key 可以自动过期的 set, ExpiringSet 和 ExpiringCacheSet 的区别是 ExpiringCacheSet 的 key 是存在 CacheManager 的缓存中

private static ExpiringCacheSet<String> checkList = new ExpiringCacheSet<String>("AuthCodeURLHandler", 60 * 5, 60, true);

# PlatformCache

平台相关的缓存项,包括用户、角色、用户角色关联

# PlatformUtil

Platform 基础平台插件工具类

PlatformUtil.getCodeMap() // 将代码项(数据字典)转成map
PlatformUtil.getRoleCodesByUserName() // 获取用户角色
PlatformUtil.getRoleName() // 获取角色名称

# Page 和 Query

mybatis-plus 分页查询工具

# SqlFilterUtils 和 AntiSQLFilter

sql 注入检测工具

# NoUtil

生成内部编码工具类

/ 用于生成这种编码格式
// 0001
// 00010001
// 000100010001
NoUtil.getMaxNo("BranchInnerCode", 4)

# PasswordUtil

加密工具类

# ApiAssert

断言工具,可以直接返回错误信息

# JWTTokenUtils

token 生成工具类

# SpringUtil

获取 spring bean 工具类

# HtmlFilterUtils

xss 攻击过滤工具类

# JacksonUtils

Jackson 工具类

# RequestUtils

Request 工具类

# ResponseUtils

Response 工具类

# Convert

Bean 转换类,用于实体类互转,使用 modelmapper 实现,只要实体继承次类即可

@Getter
@Setter
public class CodeParm extends Convert {
    /**
     * 代码类别
     */
    @NotBlank(groups = {Create.class, Update.class, ItemCreate.class, ItemUpdate.class}, message = "代码类别不能为空")
    private String codeType;
}

// Code code = codeParm.convert(Code.class);

# OperateReport

操作结果类,常用于方法返回值是成功或失败的判断上

/**
* 用户管理页修改用户密码
* @return
*/
@Priv(UserManagerPriv.ChangePassword)
@PutMapping("/password")
public ApiResponses<String> modifyPassword() {
    OperateReport operateReport = changePassword(false);
    if (operateReport.isSuccess()) {
        return success("修改成功");
    } else {
        return failure(operateReport.getMessage());
    }
}

# MysqlGenerator

mybatis-plus代码生成器, 可以自己调整

package com.ssrs.platform;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.converts.MySqlTypeConvert;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
import com.baomidou.mybatisplus.generator.config.rules.IColumnType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.*;

/**
 * @Description: 代码生成器
 * @Author: ssrs
 * @CreateDate: 2019/9/7 20:20
 * @UpdateUser: ssrs
 * @UpdateDate: 2019/9/7 20:20
 * @Version: 1.0
 */
public class MysqlGenerator {


    public static void main(String[] args) {
        String path = "G:\\EightRoes\\EightRoes\\plugin-platform\\";
        String jdbc = "jdbc:mysql://127.0.0.1:3306/eight-roes?characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false";
        String[] include = new String[]{"sys_schedule"}; // 要生成的表名
        generator(path, jdbc, include);
    }

    /**
     * <p>
     * MySQL generator
     * </p>
     */
    public static void generator(String path, String jdbc, String[] include) {
        // 自定义需要填充的字段
        List<TableFill> tableFillList = new ArrayList<>();
        tableFillList.add(new TableFill("create_user", FieldFill.INSERT));
        tableFillList.add(new TableFill("create_time", FieldFill.INSERT));
        tableFillList.add(new TableFill("update_user", FieldFill.UPDATE));
        tableFillList.add(new TableFill("update_time", FieldFill.UPDATE));
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator().setGlobalConfig(
                // 全局配置
                new GlobalConfig()
                        .setOutputDir(path + "src\\main\\java")//输出目录
                        .setFileOverride(false)// 是否覆盖文件
                        .setActiveRecord(false)// 开启 activeRecord 模式
                        .setEnableCache(false)// XML 二级缓存
                        .setBaseResultMap(false)// XML ResultMap
                        .setBaseColumnList(false)// XML columList
                        .setKotlin(false) //是否生成 kotlin 代码
                        .setAuthor("ssrs") //作者
                        //自定义文件命名,注意 %s 会自动填充表实体属性!
                        .setEntityName("%s")
                        .setMapperName("%sMapper")
                        .setXmlName("%sMapper")
                        .setServiceName("I%sService")
                        .setServiceImplName("%sServiceImpl")
                        .setControllerName("%sController")
        ).setDataSource(
                // 数据源配置
                new DataSourceConfig()
                        .setDbType(DbType.MYSQL)// 数据库类型
                        .setTypeConvert(new MySqlTypeConvert() {
                            @Override
                            public IColumnType processTypeConvert(GlobalConfig globalConfig, String fieldType) {

                                if (fieldType.toLowerCase().contains("bit")) {
                                    return DbColumnType.BOOLEAN;
                                }
                                if (fieldType.toLowerCase().contains("tinyint")) {
                                    return DbColumnType.BOOLEAN;
                                }
                                if (fieldType.toLowerCase().contains("datetime")) {
                                    return DbColumnType.LOCAL_DATE_TIME;
                                }
                                if (fieldType.toLowerCase().contains("date")) {
                                    return DbColumnType.LOCAL_DATE;
                                }
                                if (fieldType.toLowerCase().contains("time")) {
                                    return DbColumnType.LOCAL_TIME;
                                }
                                return super.processTypeConvert(globalConfig, fieldType);
                            }
                        })
                        .setDriverName("com.mysql.cj.jdbc.Driver")
                        .setUsername("root")
                        .setPassword("1234")
                        .setUrl(jdbc)
        ).setStrategy(
                // 策略配置
                new StrategyConfig()
                        .setRestControllerStyle(true)
                        .setCapitalMode(false)// 全局大写命名
                        .setTablePrefix("sys_")// 去除前缀
                        .setNaming(NamingStrategy.underline_to_camel)// 表名生成策略
                        .setInclude(include) // 需要生成的表
                        // 自动填充字段
                        .setTableFillList(tableFillList)
                        // 【实体】是否生成字段常量(默认 false)
                        .setEntityColumnConstant(true)
                        // 【实体】是否为构建者模型(默认 false)
                        .setEntityBuilderModel(false)
                        // 【实体】是否为lombok模型(默认 false)<a href="https://projectlombok.org/">document</a>
                        .setEntityLombokModel(true)
                        // Boolean类型字段是否移除is前缀处理
                        .setEntityBooleanColumnRemoveIsPrefix(true)
                        .setRestControllerStyle(true)
                // .setControllerMappingHyphenStyle(true)
        ).setCfg(
                // 注入自定义配置,可以在 VM 中使用 cfg.abc 设置的值
                new InjectionConfig() {
                    @Override
                    public void initMap() {
                        Map<String, Object> map = new HashMap<>();
                        this.setMap(map);
                    }
                }.setFileOutConfigList(Collections.<FileOutConfig>singletonList(new FileOutConfig(
                        "/templates/mapper.xml.ftl") {
                    // 自定义输出文件目录
                    @Override
                    public String outputFile(TableInfo tableInfo) {
                        return path + "src\\main\\resources\\mapper\\" + tableInfo.getEntityName() + "Mapper.xml";
                    }
                }))
        ).setPackageInfo(
                // 包配置
                new PackageConfig()
                        .setParent("com.ssrs.platform")
                        .setController("controller")
                        .setEntity("model.entity")
                        .setMapper("mapper")
                        .setService("service")
                        .setServiceImpl("service.impl")
        ).setTemplate(
                new TemplateConfig().setXml(null)
        );
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }


}