同步操作将从 LegendCraft传说工坊/legendframework-core 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
legendframework-core是一款源自游戏Minecraft中服务端Bukkit的开源的超³微量级插件开发框架 (框架仅占300kb),开发者可以更加快速便捷的开发Minecraft服务端插件,只需要导入maven坐标,即可隐形式的开发插件,并不是在脚手架中开发。如果你熟悉SpringBoot与Mybatis-Plus,那么恭喜你,你可以更容易的上手,因为本框架是这两者的重制简易版,非copy,而是在根据Bukkit开发模式下进行开发出的框架, 我们可以更加便捷的生成Yaml,对接Mysql,且支持yaml与mysql重叠事务,自动注册事件与指令,与更加便捷且优雅的开发指令。简单来说,我们制作一款带有增删改查功能的插件到打包运行,仅需5分钟, legendframework-core远比你想象的更加便捷
Git地址:
https://gitee.com/LegendPlugin/legendframework-core.git
Maven 仓库:
Maven坐标:
<project>
...
<repositories>
<!-- 先配置私服仓库地址 -->
<repository>
<id>legend</id>
<url>http://www.legendrpg.top/nexus/repository/legend/</url>
</repository>
</repositories>
<dependencies>
<!-- 导入legendframework-core -->
<dependency>
<groupId>com.legendframework</groupId>
<artifactId>legendframework-core</artifactId>
<version>1.0.2</version>
</dependency>
</dependencies>
...
</project>
若需更多支持,请移步legendframework-core交流群
为了帮助一些新手开发者上手更容易,这里需要引入三层架构的思想:
关于更多三层架构的原理与架构图可以参考: 这个链接
好了,当我们大致了解了三层架构之后可以看一下legendframework-core为我们提供了哪些便捷的能力
简单来说我们可以把IOC就当作一个Map<String,Object>的实例,这个Map存储了所有的可用Bean对象,可以集中存储,集中获取,更好的管理我们实现业务所需要用到的各种对象
@Bean("id_prop") 含义为:定义一个名称为:"id_prop" 的key,value是这个方法的返回值,存储到IOC中
注意:@Bean注解必须在类上含有@Component以及其他类似的Bean构建注解的类中才能使用
@Component()
public class Utils {
@Bean("id_prop")
public Prop xxx(){
Prop prop = new Prop();
prop.setName("xxx");
return prop;
}
}
当然我们也可以在方法入参,去获取IOC容器中的对象
利用@com.legendframework.core.ioc.annotation.Resource注解,指定从IOC中需要获取的Bean名称的对象
注意 当@Bean不给定一个名称的时候,IOC会自动以这个Bean的全限定类名作为名称进行存储
@Bean
public PlayerInfo ccc(@com.legendframework.core.ioc.annotation.Resource(name = "id_prop") Prop prop){
PlayerInfo playerInfo = new PlayerInfo();
playerInfo.setLevel(prop.getShortcutKey());
return playerInfo;
}
我们可以通过注解@Component进行标识,框架启动后会将这个类反射实例化出一个对象,并将这个对象存储到IOC中
@Component("utils")
public class Utils {}
当然我们提供的不同的实例化类型,分别用于不同场景
所以后面当我们看到类上有类似这样的注解,不必疑惑,他是一个这个类被存入了IOC中,代表你可以从IOC中取出使用
上面说到了IOC中Bean的创建,接下来是Bean的3种取出方式
@com.legendframework.core.ioc.annotation.Resource注解,必须在类上含有@Component以及其他类似的Bean构建注解的类中才能使用
@Component
public class Utils{
@Resource
private Prop prop;
}
方法入参中使用@com.legendframework.core.ioc.annotation.Resource注解,必须在类上含有@Component以及其他类似的Bean构建注解的类中,并且方法上有@Bean注解,才能使用
@Component
public class Utils{
@Bean
public Object getBean(@com.legendframework.core.ioc.annotation.Resource() Prop prop){
return new String();
}
}
public void getBean(){
//其中plugin对象为主类对象
com.legendframework.core.ioc.BeansFactory beansFactory = plugin.store.getBeansFactory();
Prop bean = beansFactory.getBean(Prop.class);
}
这里的主类,其实就是对应原生的Bukkit插件开发中的JavaPlugin的子类,只不过legendframework-core对JavaPlugin进行了封装,我们的主类所需要继承的父类不同而已
类全限定路径:com.legendframework.core.LegendPlugin
这是一个抽象的插件主类,它底层实现了加载时构建本框架初始化的操作,整个框架的入口在这个类
新建一个插件主类,必须要实现以下三个方法,
/**
* 插件被启动时执行
* 执行此方法的时候,框架已经初始化完成,所有可用Bean已经装配完毕
*/
void start();
/**
* 插件被卸载时执行
*/
void end();
/**
* 获取当前插件的根指令
* 如果这个方法你返回NULL,请务必再你插件的plugin.yml中添加一个项:commands:
* @return
*/
String getRootCmd();
/**
* 当插件被载入的时调用
* 该方法比 start() 更早执行
* 不强制要求实现
*
* 框架启动前的初始化操作初始化
* 或许你能想到实现这个方法后修改一下框架的一些参数,再让框架启动
*/
void load() {
}
/**
* 配置文件保存路径
* 希望获取到的是类似:D:/server/plugins/ 这样的绝对目录,就是插件所在目录,当然你可以对其覆写修改
* @return
*/
String getSavePath();
/**
* 是否自动代替在plugin.yml中写入指令配置
* @return 默认是true
*/
default boolean isAutowiredRegister(){
return true;
}
/**
* 获取根指令配置
* @return
*/
com.legendframework.core.cmd.CommandRootConfig getCommandRootConfig();
com.legendframework.core.cmd.CommandRootConfig ,代替了plugin.yml中command的配置
/**
* 指令相关配置
*
* 参考plugin.yml 中 commands: 配置项, 具体类见 {@link org.bukkit.command.Command}
*/
public class CommandRootConfig {
/**
* 根指令的介绍
*/
protected String description;
/**
* 当指令管理器没找到用户输入的指令时,向用户提示该信息
* 注意,该参数与{@link org.bukkit.command.Command#usageMessage} 不同
*/
protected String usageMessage;
/**
* 使用根指令的权限
*/
private String permission;
/**
* 当用户无上方权限时提示该信息
*/
private String permissionMessage;
}
插件主类必须加上**@MainPlugin**注解,加入后,可以实现Bean的自动注入,也就是说可以在主类中任意使用IOC容器中的Bean
@MainPlugin
public class TestPlugin extends LegendPlugin {
@Resource
private Prop prop;
@Override
public void start() {
System.out.println(prop);
}
@Override
public void end() {
}
@Override
public String getRootCmd() {
return null;
}
}
框架默认的配置文件的绝对路径目录是这样获取的:
//plugin是主类对象
plugin.getDataFolder().getParentFile().getAbsolutePath();
如果你想让配置文件的根目录自定义位置,可以让插件使用者在本插件.jar内中的plugin.yml配置文件中的末尾添加一项:
#声明这个插件加载后,生成的配置文件在什么目录
configPath: "D://ddd/ddd/ddd"
这里的实体类是指,对数据库表或Yaml文件所映射出来后的一个Java类,这个类封装了我们需要存储的一系列字段
作用域:类,用于声明这个类在Dao中的基本信息,表名,文件名,存储类型等,这里有提到YamlStorageType的存储类型,这个待会儿说。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Entity {
/**
* 实体类存储类型
* 定义后,该类只按照该值定义的类型来存储
*
* DataType.UNDEFINED 用于说明现目前不确定该实体类的存储类型,确切的存储类型由配置文件动态定义
* 如果在@Entity.type()中提前定义了数据存储类型例如:DataType.MYSQL ,则该实体类会无视外部配置文件的动态配置
* 每次始终使用实体类@Entity.type()中定义的存储方式
* @return
*/
DataType type() default DataType.UNDEFINED;
/**
* SQL表名
* 如果不指定,默认为本类类名驼峰转下划线,
* 例如:UserTable -> user_table
* @return
*/
String sqlTableName() default "";
/**
* yaml文件相对路径
* 如果 {@link Entity#yamlStorageType()} 取值为: YamlStorageType.MANY_TO_ONE
* 则该值应该返回一个目录(文件夹)名称,如果为其他,这个值应该为一个".yml" 后缀的文件名称,但是".yml"后缀可以不用填写
*
* @return
*/
String yamlFileName();
/**
* Yaml文件的存储类型
*
* 单个文件存储
* 一个对象代表一个Yaml文件
* ONE_TO_ONE
*
* 单个文件存储
* 一个文件存储一个集合对象
* ONE_TO_MANY
*
* 多个文件存储
* 多个文件存储,每个文件存储一个对象
* 指定目录下的所有文件,包括递归层级的文件都会读取
* MANY_TO_ONE,
*
* @return
*/
YamlStorageType yamlStorageType();
}
作用域:字段,声明这个字段是否与表进行映射以及自定义映射的名称,与yaml文件中所显示的注释信息
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
/**
* 字段别名
* 条件查询会取这个字段名
*/
String value() default "";
/**
* 是否映射到表中
* @return
*/
boolean exist() default true;
/**
* 字段注释
* value()是一个数组,所对应的就是多行注释
*/
String[] value() default "";
/**
* 是否安装字段类型来输出可选值范围
*
* 目前仅支持两种字段类型 Boolean | Enum
*
* Enum 类型字段必须实现如下接口
* @see com.legendframework.core.dao.enums.IEnum
*
* @return
*/
boolean isOutPutOptionalRange() default true;
/**
* 排序 值越小越靠前
*
* 小于1的值 代表永远排在最后
* @return
*/
int sort() default -1;
}
生成的注释示例
#无意义主键
id: "1"
#数据类型
#可选值: UNDEFINED : 不确定的类型 | YAML : yml文件存储 | MYSQL : mysql数据库存储
dataType: "YAML"
#Mysql主机地址
sqlHost:
#Mysql端口
sqlPort:
#Mysql数据库名
databaseName:
#Mysql用户名
username:
#Mysql密码
password:
作用域:字段,用于对复杂字段进行JSON序列化与反序列化的存储读取,解决了一对多表的定义
作用域:字段,与别名定义, 并且声明这个类的ID字段,实体类必须有一个ID字段,尽管这个ID在你的业务中没有任何作用
阅读上方注解**@Entity的源码可以得知,目前框架支持Yaml与Mysql两种存储方式,其中Mysql存储对于插件使用者(服主)有些许陌生了,本框架采用隐式配置在正常插件使用中,插件使用者基本上不会感觉到Mysql使用痕迹。然后就是使用最多的Yaml文件存储,基于Bukkit插件开发的惯例,.Yml文件是所有插件使用者(服主)们最熟悉的一个数据结构类型,为了更好的让插件使用者们能够更好的阅读我们插件生成的配置文件,针对Yaml文件存储,提供了三种用于Yaml的存储方式,这里针对 YamlStorageType 枚举类所对应的实际情况说明一下这三种Yaml存储方式**:
YamlStorageType.ONE_TO_ONE
一个配置文件对应一个实体类对象,多用于存储插件配置信息,示例:
id: "1"
levelConfig: 30
logConfig: DEBUG
YamlStorageType.ONE_TO_MANY
一个配置文件对应多个实体类对象,多用于存储静态化的多行信息,例如,插件道具模板列表,技能列表等,示例:
1:
id: "1"
skillName: "迅捷"
skillMp: 30
2:
id: "2"
skillName: "狂暴"
skillMp: 60
3:
id: "3"
skillName: "治疗"
skillMp: 90
YamlStorageType.MANY_TO_ONE
一个文件目录中的多个配置文件,每个配置文件对应一个实体类对象,一个文件目录就是一个实体类对象集合,多用于玩家动态的数据存储,以及可以实现YamlStorageType.ONE_TO_MANY能实现的所有功能,示例参考YamlStorageType.ONE_TO_ONE
假设定义一个日志配置类:
创建一个类名为: LogConfig 继承 AbstractEntity 类
import com.legendframework.core.dao.entity.AbstractEntity;
public class LogConfig extends AbstractEntity<LogConfig> {
}
声明**@Entity**注解信息
定义了这个实体类的yaml文件名称为"logConfig",存储类型是ONE_TO_ONE类型,这里的type值定为 DataType.YAML,意思是存储类型固定为Yaml,不会因为外部的配置动态改变
import com.legendframework.core.dao.entity.AbstractEntity;
@Entity(type = DataType.YAML,yamlFileName = "logConfig",yamlStorageType = YamlStorageType.ONE_TO_ONE)
public class LogConfig extends AbstractEntity<LogConfig> {
}
定义字段
由于父类AbstractEntity中声明了id字段,所以子类这里可以不用再次声明
这里定义了两个字段,并且用**@Column**注解进行标识了字段注释信息与排列顺序
如果枚举类LoggerLevelEnum是com.esotericsoftware.yamlbeans.extend.IEnum的子类,那么这个枚举字段会被解析自动生成 **”可选值“ **的注释信息,教程这里的枚举类并不是IEnum的子类,这里不做演示
import com.legendframework.core.dao.entity.AbstractEntity;
@Entity(type = DataType.YAML,yamlFileName = "logConfig",yamlStorageType = YamlStorageType.ONE_TO_ONE)
public class LogConfig extends AbstractEntity<LogConfig> {
@Column(
comment = {
"日志级别",
"权重: DEBUG > WARN > ERROR > INFO , 如设置为WARN , 则显示WARN及一下级别的日志信息, 不会显示DEBUG级别的信息"
},
sort = 2)
private LoggerLevelEnum logLevel;
@Column(
comment = {
"模板日志输出",
"可选值:",
" #SERVICE# : 服务名",
" #LOG_LEVEL# : 日志级别",
" #CLASS_NAME# : 类信息",
" #DATE_TIME# : 日志产生日期",
" #INFO# : 错误信息"
},
sort = 3)
private String templateInfo;
get与set方法此处省略...
}
定义默认初始参数
当插件被服务器启动后,如果第一次加载,框架会自动生成初始数据,前提是你的实体类需要是AbstractEntity的子类并且复写其方法getDefaultEntitys()
import com.legendframework.core.dao.entity.AbstractEntity;
@Entity(type = DataType.YAML,yamlFileName = "logConfig",yamlStorageType = YamlStorageType.ONE_TO_ONE)
public class LogConfig extends AbstractEntity<LogConfig> {
@Column(
comment = {
"日志级别",
"权重: DEBUG > WARN > ERROR > INFO , 如设置为WARN , 则显示WARN及一下级别的日志信息, 不会显示DEBUG级别的信息"
},
sort = 2)
private LoggerLevelEnum logLevel;
@Column(
comment = {
"模板日志输出",
"可选值:",
" #SERVICE# : 服务名",
" #LOG_LEVEL# : 日志级别",
" #CLASS_NAME# : 类信息",
" #DATE_TIME# : 日志产生日期",
" #INFO# : 错误信息"
},
sort = 3)
private String templateInfo;
/**
* 获取默认的数据
* 如果实体类实现了这个接口,会默认将这些数据保存
* @return
*/
@Override
public List<LogConfig> getDefaultEntitys() {
LogConfig log = new LogConfig();
log.setId("1");
log.setLogLevel(LoggerLevelEnum.DEBUG);
log.setTemplateInfo("[#SERVICE#][#LOG_LEVEL#][#CLASS_NAME#][#DATE_TIME#]: #INFO#");
return Collections.singletonList(log);
}
get与set方法此处省略...
}
经过Dao配置后插件自动生成的Yaml文件如下:
#无意义主键
id: "1"
#日志级别
#权重: DEBUG > WARN > ERROR > INFO , 如设置为WARN , 则显示WARN及一下级别的日志信息, 不会显示DEBUG级别的信息
logLevel: "DEBUG"
#模板日志输出
#可选值:
# #SERVICE# : 服务名
# #LOG_LEVEL# : 日志级别
# #CLASS_NAME# : 类信息
# #DATE_TIME# : 日志产生日期
# #INFO# : 错误信息
templateInfo: "[#SERVICE#][#LOG_LEVEL#][#CLASS_NAME#][#DATE_TIME#]: #INFO#"
本框架Dao层已经封装了一系列默认的CRUD(增删改查)接口,调用只需要做下Dao与Entity的绑定即可,并且在框架启动后,如果第一次启动,会按需自动创建数据库,以及与实体类中注解@Entity所指定映射的表
com.legendframework.core.dao.IDao 中定义了很多接口,这些接口均由接口实现我们可以看看都有哪些方法
public interface IDao<T> extends IConfigDao<T> , IReload , CacheFlush{
/**
* 获取实体类类型
* @return
*/
Class<T> getEntityClass();
/**
* 插入一条记录
*
* @param entity 实体对象
*/
int insert(T entity);
/**
* 根据 ID 删除
*
* @param id 主键ID
*/
int deleteById(Serializable id);
/**
* 根据 entity 条件,删除记录
*
* @param wrapper 实体对象封装操作类(可以为 null)
*/
int delete(Wrapper<T> wrapper);
/**
* 删除(根据ID 批量删除)
*
* @param idList 主键ID列表(不能为 null 以及 empty)
*/
int deleteBatchIds(Collection<? extends Serializable> idList);
/**
* 根据 ID 修改
*
* @param entity 实体对象
*/
int updateById(T entity);
/**
* 根据 whereEntity 条件,更新记录
*
* @param entity 实体对象 (set 条件值,可以为 null)
* @param updateWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
*/
int update(T entity, Wrapper<T> updateWrapper);
/**
* 根据 ID 查询
*
* @param id 主键ID
*/
T selectById(Serializable id);
/**
* 查询(根据ID 批量查询)
*
* @param idList 主键ID列表(不能为 null 以及 empty)
*/
List<T> selectBatchIds(Collection<? extends Serializable> idList);
/**
* 根据 entity 条件,查询一条记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
* @param throwEx 有多个 result 是否抛出异常
*/
T selectOne(Wrapper<T> queryWrapper, boolean throwEx);
/**
* 根据 Wrapper 条件,查询总记录数
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
long selectCount(Wrapper<T> queryWrapper);
/**
* 根据 entity 条件,查询全部记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<T> selectList(Wrapper<T> queryWrapper);
/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件(可以为 RowBounds.DEFAULT)
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
Page<T> selectPage(Page<T> page, Wrapper<T> queryWrapper);
}
当我们使用Dao进行条件筛选操作的时候,需要传入一个条件构造器com.legendframework.core.dao.wrapper.Wrapper,这是一个接口,本框架默认提供了两种条件构造器的实现类,然后我们可以通过面向对象的方式进行构建条件,下面所有关于条件构造器的案例,均是链式编程
。
条件构造器接口是指对 条件 与 条件连接 的构造
"=" , "!=" , "<" , ">" , "like" , "IS NOLL" . 等等一系列的条件符号, 表示方式为: Value1 ? Value2 , 其中 ? 则代表条件比较符
条件比较符所提供的所有方法如下:
eq(); //等于
ne(); //不等于
gt(); //大于
ge(); //大于等于
lt(); //小于
le(); //小于等于
in(); //指当前查询的值是否包含给定的一个比较值的集合,同等于SQL语法中的 IN (value1,value2,value3....)
notIn(); //与in()含义相同,不过这个是取反,指当前查询的值是否均不包含在给定一个比较值的集合中
like(); //模糊比较,比较提供的值,是否包含在值中,同等于SQL语法中的 LIKE
notLike(); //模糊比较取反
likeLeft(); //比较是否值是否以某值开头
likeRight(); //比较值只是否以某值结尾
isNull(); //是否为NULL
isNotNull; //是否不为NULL
注:其中方法入参中
boolean condition
参数如果为false,则不进行条件的校验
eq(R column, Object val)
eq(boolean condition, R column, Object val)
例: eq("name", "老王")
--->name = '老王'
ne(R column, Object val)
ne(boolean condition, R column, Object val)
例: ne("name", "老王")
--->name != '老王'
gt(R column, Object val)
gt(boolean condition, R column, Object val)
例: gt("age", 18)
--->age > 18
ge(R column, Object val)
ge(boolean condition, R column, Object val)
例: ge("age", 18)
--->age >= 18
lt(R column, Object val)
lt(boolean condition, R column, Object val)
例: lt("age", 18)
--->age < 18
le(R column, Object val)
le(boolean condition, R column, Object val)
例: le("age", 18)
--->age <= 18
in(R column, Object... values)
in(boolean condition, R column, Object... values)
例: in("age", 1, 2, 3)
--->age in (1,2,3)
notIn(R column, Object... values)
notIn(boolean condition, R column, Object... values)
例: notIn("age", 1, 2, 3)
--->age not in (1,2,3)
like(R column, Object val)
like(boolean condition, R column, Object val)
例: like("name", "王")
--->name like '%王%'
notLike(R column, Object val)
notLike(boolean condition, R column, Object val)
例: notLike("name", "王")
--->name not like '%王%'
likeLeft(R column, Object val)
likeLeft(boolean condition, R column, Object val)
例: likeLeft("name", "王")
--->name like '%王'
likeRight(R column, Object val)
likeRight(boolean condition, R column, Object val)
例: likeRight("name", "王")
--->name like '王%'
isNull(R column)
isNull(boolean condition, R column)
例: isNull("name")
--->name is null
isNotNull(R column)
isNotNull(boolean condition, R column)
例: isNotNull("name")
--->name is not null
"and" , "or" 连接条件只有这两种,and代表并且(&&),or代表或者(||) , 他们拼接在 条件与条件之间,表示方式:
条件1
&&条件2
and()
and(boolean condition)
框架默认是使用的and拼接符,下面这两个调用方式是等价的
例1: eq("id",1).eq("name","老王")
--->id = 1 and name = '老王'
例2: eq("id",1).and().eq("name","老王")
--->id = 1 and name = '老王'
and(Function<Param, Param> func)
and(boolean condition, Function<Param, Param> func)
这个Function函数式接口中的入参,框架会传递一个新的条件构造器
例: eq("id",1).and(fun -> fun.eq("name","老王").or().eq("name","你儿子"))
---> id = 1 and (name = "老王" or name = "你儿子")
or()
or(boolean condition)
注意事项:
主动调用
or
表示紧接着下一个方法不是用and
连接!(不调用or
则默认为使用and
连接)
例: eq("id",1).or().eq("name","老王")
--->id = 1 or name = '老王'
or(Function<Param, Param> func)
or(boolean condition, Function<Param, Param> func)
例: eq("name","老王").or(fun -> fun.eq("id",1).eq("name","你儿子"))
---> name = "老王" or (id = 1 eq name = "你儿子")
这是一个基础的条件构造器, 上面的条件构造案例均是使用的 QueryWrapper 类所写出的案例
通过 QueryWrapper 的使用之后,我们会发现一个问题,就是需要我们主动输入字段名称,这可能会带来一些问题,例如输错,未来字段变更后代码编译是没有报错的,所以有了 LambdaQueryWrapper
两者区别:
//首先从Wrappers类中获取条件构造器
//普通方式
QueryWrapper<T> query();
//lambda方式
LambdaQueryWrapper<T> lambdaQuery();
// 等价示例:
query().eq("id", value);
lambdaQuery().eq(Entity::getId, value);
其中,所代替 QueryWrapper 的第一个参数,是传入 LambdaQueryWrapper 泛型中的类型,然后通过这个类型利用Jdk8新特性::
选择相应的get方法,框架会自动解析这个get方法,获取这个字段名称
直接使用com.legendframework.core.dao.wrapper.defaults.Wrappers这个类的静态方法即可获取实例
//获取QueryWrapper条件构造器
QueryWrapper<T> queryWrapper = com.legendframework.core.dao.wrapper.defaults.Wrappers.query();
//获取LambdaQueryWrapper条件构造器
LambdaQueryWrapper<T> queryWrapper = com.legendframework.core.dao.wrapper.defaults.Wrappers.lambdaQuery();
通过传入Page对象,指定页码与每页显示条目数,即可分页
/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
Page<T> selectPage(Page<T> page, Wrapper<T> queryWrapper);
Page类
public class Page<T> implements Serializable {
/*页码(1开始)*/
private long currentPage
/*每页条目数*/
private long itemNum;
/*总页数*/
private long totalPage;
/*总记录数*/
private long totalSum;
/*分页后数据*/
private List<T> list;
getset.....
}
Dao的定义非常的简单,只需要创建一个Dao接口,继承IDao即可,并且在IDao的T泛型中填入我们实体类的类名即可
最关键的一点是,需要在我们定义的Dao上加入**@EntityDao**注解,这样才能让这个Dao生效且自动生成CRUD接口方法
import com.legendframework.core.dao.annotation.EntityDao;
import com.legendframework.core.dao.IDao;
@EntityDao
public interface LogConfigDao extends IDao<LogConfig> {
}
我们使用上方定义的插件主类来使用这个Dao,通过 @Resource 注解就可以把这个 LogConfigDao 注入进去然后使用
@MainPlugin
public class TestPlugin extends LegendPlugin {
@Resource
private LogConfigDao logConfigDao;
@Override
public void start() {
//这里就可以调用dao接口中的方法,选择你要调用的方法进行使用了
LambdaQueryWrapper<LogConfig> warpper = Wrappers.lambdaQuery();
LogConfig logConfig = logConfigDao.selectOne(warpper.eq(LogConfig::getLogLevel, LoggerLevelEnum.DEBUG),false);
}
........
}
Service默认提供了一系列对Dao层接口的封装接口,可以理解为Service内包装了一个Dao,在三层架构中,每层各司其职,不越界,低耦合高内聚的思想,Dao层只处理数据读写的逻辑,其他业务相关的逻辑由Service来处理,与用户交互的逻辑由Controller来处理 , Controller负责组装调用Service
Service提供的默认接口
public interface IService<T> extends IConfigService<T> , IReload {
/**
* 插入一条记录(选择字段,策略插入)
*
* @param entity 实体对象
*/
boolean save(T entity);
/**
* 插入一条具有默认数据的记录 (主动触发)
*
* 如果这个ID的记录不存在,则获取去获取这个实体类的默认配置,然后进行赋值ID插入
*
* 适用于玩家初次进入服务器,进而生成玩家配置文件
*
* @param id
* @return
*/
boolean saveDefault(Serializable id);
/**
* 根据 ID 删除
*
* @param id 主键ID
*/
boolean removeById(Serializable id);
/**
* 根据 entity 条件,删除记录
*
* @param queryWrapper 实体包装类 {@link com.legendframework.core.dao.wrapper.defaults.QueryWrapper}
*/
boolean remove(Wrapper<T> queryWrapper);
/**
* 删除(根据ID 批量删除)
*
* @param idList 主键ID列表
*/
boolean removeByIds(Collection<? extends Serializable> idList);
/**
* 根据 ID 选择修改
*
* @param entity 实体对象
*/
boolean updateById(T entity);
/**
* 根据 whereEntity 条件,更新记录
*
* @param entity 实体对象
*/
boolean update(T entity, Wrapper<T> updateWrapper);
/**
* 根据 ID 查询
*
* @param id 主键ID
*/
T getById(Serializable id);
/**
* 查询(根据ID 批量查询)
*
* @param idList 主键ID列表
*/
List<T> listByIds(Collection<? extends Serializable> idList);
/**
* 根据 Wrapper,查询一条记录
*
* @param queryWrapper 实体对象封装操作类 {@link com.legendframework.core.dao.wrapper.defaults.QueryWrapper}
*/
default T getOne(Wrapper<T> queryWrapper) {
return getOne(queryWrapper, false);
}
/**
* 根据 Wrapper,查询一条记录
*
* @param queryWrapper 实体对象封装操作类 {@link com.legendframework.core.dao.wrapper.defaults.QueryWrapper}
* @param throwEx 有多个 result 是否抛出异常
*/
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
/**
* 根据 Wrapper,查询一条记录
*
* @param queryWrapper 实体对象封装操作类 {@link com.legendframework.core.dao.wrapper.defaults.QueryWrapper}
*/
Object getObj(Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询总记录数
*
* @param queryWrapper 实体对象封装操作类 {@link com.legendframework.core.dao.wrapper.defaults.QueryWrapper}
*/
long count(Wrapper<T> queryWrapper);
/**
* 查询列表
*
* @param queryWrapper 实体对象封装操作类 {@link com.legendframework.core.dao.wrapper.defaults.QueryWrapper}
*/
List<T> list(Wrapper<T> queryWrapper);
/**
* 翻页查询
*
* @param page 翻页对象
* @param queryWrapper 实体对象封装操作类 {@link com.legendframework.core.dao.wrapper.defaults.QueryWrapper}
*/
Page<T> page(Page<T> page, Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询全部记录
*
* @param queryWrapper 实体对象封装操作类 {@link com.legendframework.core.dao.wrapper.defaults.QueryWrapper}
*/
List<Object> listObjs(Wrapper<T> queryWrapper);
}
这里只说明定义方式,调用方式与调用IOC容器中Bean的方式一致,注解注入即可
public abstract class BaseService<T,D extends IDao<T>> implements IService<T> {...}
让自己的Service去继承BaseService
,泛型1的参数填写为对应的实体类,泛型2参数填写为对应的Dao接口
public class PropService extends BaseService<Prop,PropDao> {...}
最重要的一步,给自己的Service类上加上**@Service**注解
@Service
public class PropService extends BaseService<Prop,PropDao> {...}
这是一个单泛型的Service,只需要开发者传入一个实体类即可使用增删改查的所有功能,内部封装了获取Dao的操作
public abstract class VagueService<T> extends BaseService<T, IDao<T>> {...}
同样记住加上**@Service**注解
@Service
public class PropService extends VagueService<Prop> {...}
可以在**@Service所标识类中的任意方法中使用@Transactional**注解即可,对整个方法的调用链进行相应逻辑的事务控制,支持事务嵌套,支持Yaml与Mysql的事务合并,其中Yaml的事务隔离级别为:串行化
@Service
public class PropService extends VagueService<Prop> {
@Transactional
public void account(){
Prop prop = getOne();
prop.setName("修改值1");
updateById(prop);
//制造异常
throw new RuntimeException("手动异常");
}
}
注意:在方法中调用本类的带有**@Transactional**的方法,这时这个被调用的方法的事务注解将不会生效,具体原因是动态代理的问题,这里就不进阶讲解了,大家知道有这一回事就好
框架支持4种事务类型
REQUIRED
支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择
SUPPORTS
支持当前事务,如果当前没有事务,就以非事务方式执行
REQUIRES_NEW
新建事务,如果当前存在事务,把当前事务挂起
NOT_SUPPORTED
以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
原生的Bukkit事件注册需要手动在主类中进行注册,现框架为您省略了这一步操作,你只需要在事件的类上加上**@Event**注解即可自动注册事件
@Event
public class MyPlayerJoinEvent implements Listener {
/**
* 玩家进入服务器事件
* @param e
*/
@EventHandler
public void event(PlayerJoinEvent e){
e.setJoinMessage("你好,欢迎进入服务器!");
}
}
相比事件,指令就没这么简单就能讲完了,我们先来看下原生的指令定义:
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0){
//当前输入的是根指令
return true;
}
if (args.length == 1){
if (StringUtils.equals(args[0],"info")){
//xxx info
//查看玩家个人信息
} else if (StringUtils.equals(args[0], "reload")) {
//xxx reload
//重载插件
}
return true;
}
if (args.length == 2){
if (StringUtils.equals(args[0],"info")){
//xxx info <玩家名称>
//查看指定玩家的个人信息
String name = args[1];
}
return true;
}
return false;
}
由于原生的指令,是在一个**onCommand()**方法中进行判断处理插件的指令,如果是指令少还好,如果指令一多,复杂的if,else条件就会让人看得眼花缭乱,可读性极低,修改BUG,每次都要阅读代码很久才能定位问题,并且每次都要处理各种各样的参数转换,校验等操作。来看看本框架是如何定义指令的
@Command(cmd = "my",title = "我的指令")
public class MyCommand extends BaseCommand {
@Cmd(value = "info",title = "查看玩家信息",desc = "",isAsyn = true)
public void info(@CmdParam(
title = "玩家",
desc = "如果不传入参数则查询自己信息",
demo = "196",
required = false) Player player){
if (player == null) {
//查询自己的信息
}else {
//查询指定玩家信息
}
}
@Cmd(value = "reload",title = "重载插件")
public void reload(){
System.out.println("重载插件...");
}
}
接下来对框架的指令类进行详解
每个指令类都必须有一个**@Command**注解,用于将Bean存入IOC中,并且定义这个指令的前缀,标题等信息
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Command {
/**Bean名称*/
String value() default "";
/**
* 指令前缀
* 设置了这个参数的指令类中的指令方法的cmd前缀都会加上这个参数的值
* @return
*/
String cmd();
/**指令类标题*/
String title();
}
框架的指令文档输出,是根据指令树形结构来生成的,我们如果需要用到指令的文档输出,就必须要了解树形结构,从下图中可以看出,根指令为唯一,往下延申,指令类可以有多个子指令,并且子指令可以被多个父指令所拥有,每一个指令类中的指令方法都代表一个指令,指令类代表一个指令方法的集合
<img alt="image description">
/**
* 指令帮助的树形结构节点注解
* 使用它框架能帮助您实现指令help的树形图
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CommandParentNode {
/**父节点*/
Class<? extends BaseCommand>[] value();
}
根指令的定义方式,使用**@Command指定cmd()**参数为空串,根指令的cmd()参数必须是空串
则这个指令类中定义的所有方法的指令均为:
/根指令 ?? ?? ??
。其中/根指令
代表你在主类中定义的getRootCmd()值,??
代表你方法注解@Cmd定义的指令路径
@Command(title = "根指令" , cmd = "")
public class MyRootCommand extends BaseCommand {...}
子指令定义的方式
使用注解CommandParentNode , 指定自己的父指令是哪些,可以有多个
@Command(title = "一级指令" , cmd = "one")
@CommandParentNode(MyRootCommand.class)
public class MyPropCommand extends BaseCommand {...}
把指令类当作一个指令的集合,那么这个类中的被**@Cmd**标识的方法就是这个集合中的一个指令
@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Cmd {
/**指令*/
String[] value();
/**指令标题信息*/
String title() default "";
/**指令描述信息*/
String desc() default "";
/**是否异步执行*/
boolean isAsyn() default true;
}
为了帮助更好的理解指令的路径拼接,举两个例子:
例1:
假设插件的主类中定义的根指令**getRootCmd()**为:
test
定义一个根指令的指令类,根指令的cmd参数必须是空串,代表访问到这个指令类中方法的路径为:
/test ??
@Command(title = "根指令" , cmd = "") public class MyRootCommand extends BaseCommand { @Cmd(value="cmd1",title="指令1") public void cmd1(){...} }
那么输入
/test cmd1
就可以执行指令1
例2:
在
例1
的基础上在新增一个指令类,作为根指令的子指令,这个子指令指定根指令的指令类
为父指令
@Command(title = "玩家相关指令" , cmd = "player") @CommandParentNode(MyRootCommand.class) public class MyPlayerCommand extends BaseCommand { @Cmd(value="info",title="查看玩家信息") public void cmd1(){...} }
那么输入
/test player info
就可以执行info
指令
作用在方法上
这个指令只能由Op身份的玩家执行
@Cmd(value="test",title="测试指令")
@OnlyOp
public void test(){...}
作用在方法上
这个指令只能由玩家执行
@Cmd(value="test",title="测试指令")
@OnlyPlayer
public void test(){...}
作用在方法上
这个指令只能由服务器执行
@Cmd(value="test",title="测试指令")
@OnlyServer
public void test(){...}
作用在方法上
声明玩家执行这个指令所需的权限
如果**@Permission的value()不填权限默认为: 根指令 + 指令类指令 + 方法指令 如果当前方法的@Cmd**定义了有多条指令,则默认取第一条解析为权限 即为完整指令,空格替换为'.'
/**
* 例根指令为 'test'
*/
@Command(title = "玩家相关指令" , cmd = "player")
@CommandParentNode(MyRootCommand.class)
public class MyPlayerCommand extends BaseCommand {
@Cmd(value="info",title="查看信息")
@Permission
public void info(@CmdParam(title="玩家名称") String name){...}
@Cmd(value="give",title="给予玩家物品")
@Permission("abc.give")
public void give(){...}
}
经过框架解析后
执行
info
所需的权限为:test.player.info
执行
give
所需的权限为:abc.give
作用在
方法
,字段
声明指令方法中的参数
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface CmdParam {
/**参数标题信息*/
String title() default "";
/**参数描述信息*/
String desc() default "";
/**参数示例*/
String demo() default "";
/**是否必须*/
boolean required() default true;
}
来个例子帮助理解:
/** * 例根指令为 'test' */ @Command(title = "玩家相关指令" , cmd = "player") @CommandParentNode(MyRootCommand.class) public class MyPlayerCommand extends BaseCommand { @Cmd(value="upExp",title="给一个玩家提升经验值") @OnlyOp @Permission public void upExp( @CmdParam(title="给予的经验") Long exp, @CmdParam(title="玩家名称",desc="不传默认为自己",required=false) String name ){...} }
/test player upExp 经验值 玩家名称
与/test player upExp 经验值
均可以执行到这个指令方法
**注意: **上面的例子中第二个参数,**required()**设置的false,代表这个参数非必填。
**required()**生效有两个条件:
1. 这个参数在参数末尾
2. 这个参数后面的参数的required()值均为false
在指令类的每个方法,因为有**@CmdParam**注解的存在,会显得每个方法的参数非常的臃肿,可读性低,分不清入参与代码块,为了提高代码的简洁度与可读性,我们想将这么多的入参全部封装到一个Bean中时,就需要用到这个注解
public class PlayerUpExpParam {
@CmdParam(title="给予的经验")
private Long exp;
@CmdParam(title="玩家名称",desc="不传默认为自己",required=false)
private String name;
getset...
}
/**
* 例根指令为 'test'
*/
@Command(title = "玩家相关指令" , cmd = "player")
@CommandParentNode(MyRootCommand.class)
public class MyPlayerCommand extends BaseCommand {
@Cmd(value="upExp",title="给一个玩家提升经验值")
@OnlyOp
@Permission
public void upExp(@CmdCmdEntityParam PlayerUpExpParam param){...}
}
调用路径与**@CmdParam**的例子一致:
/test player upExp 经验值 玩家名称
与/test player upExp 经验值
均可以执行到这个指令方法
作用在方法入参与字段上,用于限制一个
字符串
,数值
类型的大小
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Size {
/**最小值*/
long min() default 0;
/**最大值*/
long max() default Long.MAX_VALUE;
/**超过范围后的提示内容*/
String message() default "取值范围有误";
}
作用在方法入参与字段上,用于限制一个集合的大小
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface CollectionSize {
/**最大值*/
int max() default Integer.MAX_VALUE;
/**超过范围后的提示内容*/
String message() default "集合大小超过了给定的最大值";
}
作用在方法,类上
指令执行成功处理器注解,当指令方法执行成功后,会自动执行
com.legendframework.core.cmd.BaseCommand#successHandle()
方法如果作用在类上,说明这个指令类的所有指令方法执行成功后都会执行**successHandle()**方法
作用在方法,类上
指令执行失败处理器注解,当指令方法异常后,会自动执行
com.legendframework.core.cmd.BaseCommand#failHandle()
方法如果作用在类上,说明这个指令类的所有指令方法执行异常后都会执行**failHandle()**方法
作用在方法,类上,使用这个注解,相当于同时使用了**@CmdSuccessHandle与@CmdFailHandle**
不使用注解就会自动注入的数据类型:
- org.bukkit.command.CommandSender
- org.bukkit.command.Command
例:
@Cmd(value="upExp",title="给一个玩家提升经验值") @OnlyOp @Permission public void upExp(CommandSender sender , @CmdCmdEntityParam PlayerUpExpParam param , Command cmd){...}
如果声明了这两个参数,框架会自动将这个两个内置类型,注入到指令方法中
@CmdParam注解所支持的数据类型有:
当然你也可以自定义参数解析,新建一个类实现com.legendframework.core.cmd.paramer.converter.ParamerConverter
接口,并且将类上加上**@Component**注解
例:
@Component
public class PlayerParamerConverter implements ParamerConverter<Player> {
/**
* 校验这个行参是否符合这个转换器
* @return
*/
@Override
public boolean check(Class<?> cls) {
return Player.class == cls;
}
/**
* 转换类型
*
* 不要以 {@link IParameter} 为需转换的类型
* 真实的需要转换类型取 type , 因为考虑到泛型List等情况
* {@link IParameter} 作用是取其注解进行扩展功能
*
* @param sender 发送者
* @param message 消息
* @param type 需要转换的类
* @param parameter parameter
* @return
*/
@Override
public Player castParameter(CommandSender sender, String message, Class<Player> type, IParameter parameter) {
return Bukkit.getPlayer(message);
}
}
com.legendframework.core.cmd.BaseCommand
是框架定义的一个实现指令树形结构,且封装了指令帮助输出抽象逻辑,我们开发者定义的所有指令类都应该是BaseCommand的子类
/**
* 通一的执行成功处理器
* 当某指令方法上被标识了@CmdSuccessHandle时,会触发该处理器
*
* @param sender 指令发送者
* @param method 执行成功的指令方法
* @param params 执行这个指令方法所传入的参数
*/
void successHandle(CommandSender sender , Method method , Object... params);
/**
* 当方法执行异常的处理器,方法执行异常时触发
*
* @param sender 指令发送者
* @param e 异常信息
* @param method 执行失败的指令方法
*/
void failHandle(CommandSender sender ,Throwable e , Method method);
/**
* 当指令参数的个数正确,但与方法所匹配的类型不一致时,会调用该处理器
*
* 例如:参数类型为int类型, 玩家输入的这个参数为 "abc" ,则会调用该处理器
*
* @param sender 指令发送者
* @param e 异常信息
* @param method 封装入参失败的指令方法
* @param parameter 导致参数错误的这个参数,它可能是一个入参 {@link Parameter} 也可能是一个字段 {@link Field}
* @param param 导致错误方法实参
* @param index 参数的索引
*/
void paramsErrorHandle(CommandSender sender , Throwable e, Method method , IParameter parameter , Object param, int index);
其实,写指令最没有技术含量又最麻烦的其实就是指令文档了,原生Bukkit插件的指令开发,更改了指令名称,相关参数都会去手动修改指令文档的输出,为了解决这些问题,整个BaseCommand中的代码,有四分之三都是在描述指令文档,现在分别介绍指令文档:
看到这里,大家都知道每一个BaseCommand的子类都是一个指令类,这个指令类中有N个指令,是封装了N个指令的集合,意味着,我每个BaseCommand的子类都可以修改其父类的默认方法,实现帮助文档的自定义化
这是每一个指令类都会带有一个help指令,这个指令是BaseCommand提供的,help指令会输出当前这个指令类下的所有子指令的help()指令,以及自身定义的指令集合
具体内容如下:
/**
* 为子类提供的help公用方法
*
* @param page 页码
* {@link Cmd} 中的指令前会拼接子类 {@link Command#cmd()} 参数
* <p>
* 例如有两个子类 分别为:PlayerCmd ,ServerCmd
* 其类上的 {@link Command#cmd()} 参数分别为 'p' , 's'
* 那么这两个子类的help指令的调用为:
* 1. PlayerCmd : '/根指令 p help' or '/根指令 p'
* 2. ServerCmd : '/根指令 s help' or '/根指令 s'
* 执行的help指令会进入到不同的子类对象的该方法 help() 中 , 子类可以复写这个方法
*/
@Cmd({"help", ""})
@CmdFailHandle
@Permission()
public void help(
CommandSender sender,
org.bukkit.command.Command command,
@CmdParam(title = "页码", desc = "数字", demo = "1", required = false) Integer page
) {
page = page == null ? 1 : page;
Command thisCommand = getThisCommandAnnotation();
List<CmdHelp> cmdHelps = getHelps();
try {
CmdHelp help = createCmdhelp(
BaseCommand.class.getMethod("help", CommandSender.class, org.bukkit.command.Command.class, Integer.class),
thisCommand,
this
);
//分页且渲染
renderingHelp(sender, command, help, PageUtil.page(cmdHelps, page, helpItemNum()));
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
注:如要复写这个help()方法,需要自行加上@Cmd()等一系列的注解,注解不会继承
这些方法均提供了默认实现,如果你需要自定义,可以进行覆写
/**
* 获取指令帮助的每页显示条目数
*/
int helpItemNum();
/**
* 渲染指令help前缀
*
* @param sender 指令执行者
* @param command org.bukkit.command.Command 指令
* @param help 当前准备渲染的指令封装类
* @param cmdHelpPage 需要渲染的指令帮助列表
*/
protected void renderingHelpPrefix(CommandSender sender, Command command, CmdHelp help, Page<CmdHelp> cmdHelpPage);
/**
* 展示指令帮助的内容部分
*
* 无需关注分页逻辑,只需要自定渲染风格
*
* @param sender 指令执行者
* @param command org.bukkit.command.Command 指令
* @param help 当前准备渲染的指令封装类
* @param cmdHelpPage 需要渲染的指令帮助列表
*/
protected void renderingHelpBody(CommandSender sender, Command command, CmdHelp help, Page<CmdHelp> cmdHelpPage);
/**
* 渲染指令help后缀
*
* @param sender 指令执行者
* @param command 指令
* @param help 当前准备渲染的指令封装类
* @param cmdHelpPage 需要渲染的指令帮助列表
*/
protected void renderingHelpSuffix(CommandSender sender, org.bukkit.command.Command command, CmdHelp help, Page<CmdHelp> cmdHelpPage);
在执行help()指令后,框架会自动获取相应页码的指令,然后依次调用前缀,内容,后缀
方法,开发者可以自定义其渲染风格
/**
* 默认渲染风格
*
* @param cmdHelpPage 需要渲染的指令帮助列表
*/
@Override
public void renderingHelp(CommandSender sender, org.bukkit.command.Command command, CmdHelp help, Page<CmdHelp> cmdHelpPage) {
if (cmdHelpPage.getTotalSum() == 0) {
sender.sendMessage("当前注册的指令: [" + command.getName() + "] , 没有绑定任何子指令");
return;
} else if (cmdHelpPage.getList().size() == 0) {
sender.sendMessage("当前页没有任何数据");
}
renderingHelpPrefix(sender, command, help, cmdHelpPage);
renderingHelpBody(sender, command, help, cmdHelpPage);
renderingHelpSuffix(sender, command, help, cmdHelpPage);
}
注:如需在指令参数中插入空格,或传入集合到指令方法参数中,需要用到'{}'符号,例如:
/test player {这是一句 含有空 格 的话 这段话 如果指令 方法中的参数是集合 这段话会以空格进行 分割 填充到集合中}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。