diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c27bdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### BUILD ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +bin/ +doc/ +target/ +out/ +.DS_Store diff --git a/README.md b/README.md index 749f7dd..36acd5d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# halo +## halo Halo框架 diff --git a/halo-base/pom.xml b/halo-base/pom.xml new file mode 100644 index 0000000..52c1957 --- /dev/null +++ b/halo-base/pom.xml @@ -0,0 +1,59 @@ + + + + + org.xujin.halo + halo + 1.0.4 + ../pom.xml + + 4.0.0 + + halo-base + 1.0.4 + halo-base + + + + + org.xujin.halo + halo-common + + + org.projectlombok + lombok + + + org.springframework + spring-core + + + org.springframework + spring-context + + + + org.reflections + reflections + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + + jar-no-fork + + + + + + + diff --git a/halo-base/src/main/java/org/xujin/halo/annotation/command/Command.java b/halo-base/src/main/java/org/xujin/halo/annotation/command/Command.java new file mode 100644 index 0000000..7760ecb --- /dev/null +++ b/halo-base/src/main/java/org/xujin/halo/annotation/command/Command.java @@ -0,0 +1,13 @@ +package org.xujin.halo.annotation.command; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Component +public @interface Command { + +} diff --git a/halo-base/src/main/java/org/xujin/halo/annotation/command/PostInterceptor.java b/halo-base/src/main/java/org/xujin/halo/annotation/command/PostInterceptor.java new file mode 100644 index 0000000..31b40d8 --- /dev/null +++ b/halo-base/src/main/java/org/xujin/halo/annotation/command/PostInterceptor.java @@ -0,0 +1,15 @@ +package org.xujin.halo.annotation.command; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +@Inherited +@Component +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PostInterceptor { + + Class[] commands() default {}; + +} diff --git a/halo-base/src/main/java/org/xujin/halo/annotation/command/PreInterceptor.java b/halo-base/src/main/java/org/xujin/halo/annotation/command/PreInterceptor.java new file mode 100644 index 0000000..694b55a --- /dev/null +++ b/halo-base/src/main/java/org/xujin/halo/annotation/command/PreInterceptor.java @@ -0,0 +1,15 @@ +package org.xujin.halo.annotation.command; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +@Inherited +@Component +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PreInterceptor { + + Class[] commands() default {}; + +} diff --git a/halo-base/src/main/java/org/xujin/halo/annotation/domian/Domain.java b/halo-base/src/main/java/org/xujin/halo/annotation/domian/Domain.java new file mode 100644 index 0000000..957caf4 --- /dev/null +++ b/halo-base/src/main/java/org/xujin/halo/annotation/domian/Domain.java @@ -0,0 +1,38 @@ +package org.xujin.halo.annotation.domian; + +import java.lang.annotation.*; + +/** + * 域描述注解 + * @author xujin + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Inherited +public @interface Domain { + + /** + * 当前域的唯一code + * @return + */ + String code(); + + /** + * 父域的唯一code + * @return + */ + String parentCode() default ""; + + /** + * 当前域的名称 + * @return + */ + String name(); + + /** + * 当前域的描述 + * @return + */ + String desc() default ""; + +} diff --git a/halo-base/src/main/java/org/xujin/halo/annotation/domian/DomainAbility.java b/halo-base/src/main/java/org/xujin/halo/annotation/domian/DomainAbility.java new file mode 100644 index 0000000..17a23ed --- /dev/null +++ b/halo-base/src/main/java/org/xujin/halo/annotation/domian/DomainAbility.java @@ -0,0 +1,18 @@ +package org.xujin.halo.annotation.domian; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * 领域能力,用来描述领域实体的行为 + * @author xujin + * + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@Component +public @interface DomainAbility { + +} diff --git a/halo-base/src/main/java/org/xujin/halo/annotation/domian/DomainService.java b/halo-base/src/main/java/org/xujin/halo/annotation/domian/DomainService.java new file mode 100644 index 0000000..c8b202b --- /dev/null +++ b/halo-base/src/main/java/org/xujin/halo/annotation/domian/DomainService.java @@ -0,0 +1,43 @@ +package org.xujin.halo.annotation.domian; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface DomainService { + + /** + * 当前域的唯一编码 + * @return + */ + String code(); + + /** + * 域服务的名称用于显示 + * @return + */ + String name(); + + /** + * 当前域的描述 + * @return + */ + String desc() default ""; + + /** + * 当前域服务的帮助url + * @return + */ + String helpUrl() default ""; + + /** + * the domain ability codes + * @return + */ + String[] abilityCodes() default ""; + + +} diff --git a/halo-base/src/main/java/org/xujin/halo/domain/DomainFactoryI.java b/halo-base/src/main/java/org/xujin/halo/domain/DomainFactoryI.java new file mode 100644 index 0000000..483feb8 --- /dev/null +++ b/halo-base/src/main/java/org/xujin/halo/domain/DomainFactoryI.java @@ -0,0 +1,12 @@ +package org.xujin.halo.domain; + +/** + * 领域工厂 + * @author xujin + * + */ +public interface DomainFactoryI { + + T create(); + +} diff --git a/halo-base/src/main/java/org/xujin/halo/domain/Entity.java b/halo-base/src/main/java/org/xujin/halo/domain/Entity.java new file mode 100644 index 0000000..d727812 --- /dev/null +++ b/halo-base/src/main/java/org/xujin/halo/domain/Entity.java @@ -0,0 +1,55 @@ +package org.xujin.halo.domain; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 聚合内的实体 + * This is the parent object of all domain objects + * @author xujin + */ +public abstract class Entity { + + /* + * Entity 包含所有表都有的6个基础字段. + */ + @Getter @Setter + protected String id; + @Getter @Setter + protected Date gmtCreate; + @Getter @Setter + protected Date gmtModified; + @Getter @Setter + protected String creator; + @Getter @Setter + protected String modifier; + @Getter @Setter + protected String isDeleted; + @Getter @Setter + protected String tenantId;//租户ID + @Getter @Setter + protected String bizCode;//业务代码 + + /* + * 扩展字段 + */ + @Getter + @Setter + protected Map extValues = new ConcurrentHashMap(); + + public T getExtField(String key){ + if(extValues != null){ + return (T)extValues.get(key); + } + return null; + } + + public void putExtField(String fieldName, Object value){ + this.extValues.put(fieldName, value); + } + +} diff --git a/halo-base/src/main/java/org/xujin/halo/domain/ServiceI.java b/halo-base/src/main/java/org/xujin/halo/domain/ServiceI.java new file mode 100644 index 0000000..c7af2dc --- /dev/null +++ b/halo-base/src/main/java/org/xujin/halo/domain/ServiceI.java @@ -0,0 +1,10 @@ +package org.xujin.halo.domain; + +/** + * 领域服务,继承自DomainServiceI的接口都认为是DomainAbility + * @author xujin + * + */ +public interface ServiceI { + +} diff --git a/halo-base/src/main/java/org/xujin/halo/domain/ValueObject.java b/halo-base/src/main/java/org/xujin/halo/domain/ValueObject.java new file mode 100644 index 0000000..4190b8b --- /dev/null +++ b/halo-base/src/main/java/org/xujin/halo/domain/ValueObject.java @@ -0,0 +1,10 @@ +package org.xujin.halo.domain; + +/** + * 领域值对象 + * @author xujin + * + */ +public interface ValueObject { + +} diff --git a/halo-collection/pom.xml b/halo-collection/pom.xml new file mode 100644 index 0000000..1b5bab2 --- /dev/null +++ b/halo-collection/pom.xml @@ -0,0 +1,59 @@ + + + + + org.xujin.halo + halo + 1.0.4 + ../pom.xml + + 4.0.0 + + halo-collection + + halo-collection + + + + + org.xujin.halo + halo-base + + + org.projectlombok + lombok + + + org.springframework + spring-core + + + org.springframework + spring-context + + + + org.reflections + reflections + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + + jar-no-fork + + + + + + + diff --git a/halo-collection/src/main/java/org/xujin/halo/domain/AbilityCollection.java b/halo-collection/src/main/java/org/xujin/halo/domain/AbilityCollection.java new file mode 100644 index 0000000..dc05d11 --- /dev/null +++ b/halo-collection/src/main/java/org/xujin/halo/domain/AbilityCollection.java @@ -0,0 +1,246 @@ +package org.xujin.halo.domain; + +import lombok.Setter; +import org.reflections.Reflections; +import org.reflections.scanners.MethodAnnotationsScanner; +import org.reflections.scanners.SubTypesScanner; +import org.reflections.util.ConfigurationBuilder; +import org.springframework.stereotype.Component; +import org.xujin.halo.annotation.domian.DomainAbility; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 采集系统的能力图 + * @author xujin + * + */ +@Component +public class AbilityCollection { + + @Setter + private List packages = new ArrayList<>(); + + /** + * 添加扫描范围 + * @param pkg + */ + public void addPackage(String pkg){ + packages.add(pkg); + } + + /** + * 收集能力图 + */ + public AbilityGraph collectAbility(){ + Reflections reflections = new Reflections(new ConfigurationBuilder() + .forPackages(packages.stream().toArray(String[]::new)) + .addScanners(new MethodAnnotationsScanner(), new SubTypesScanner())); + final AbilityGraph g = new AbilityGraph(); + //扫描领域能力 + Set methods = reflections.getMethodsAnnotatedWith(DomainAbility.class); + graphMethod(g, methods); + //扫描领域实体 + Set> entities = reflections.getSubTypesOf(Entity.class); + graphEntity(g, entities); + //扫描领域服务 + Set> services = reflections.getSubTypesOf(ServiceI.class); + graphService(g, services); + return g; + } + private void graphMethod(final AbilityGraph g, Set methods) { + if(methods != null){ + methods.forEach(m -> { + String ability = m.getName(); + g.attribute.add(ability + AbilityGraph.POINT);//能力用点表示 + Class clazz = m.getDeclaringClass(); + String domain = clazz.getSimpleName(); + if(Entity.class.isAssignableFrom(clazz)){ + g.attribute.add(domain + AbilityGraph.BOX);//实体用矩形表示 + } + if(ValueObject.class.isAssignableFrom(clazz)){ + g.attribute.add(domain + AbilityGraph.DOTBOX);//值用虚线矩形表示 + } + //当前Entity或ValueObject拥有哪些Ability + g.edges.add(AbilityGraph.Edge.of(ability, domain).type(AbilityGraph.Edge.TYPE.normal).direct(AbilityGraph.Edge.DIR.back));//拥有哪些能力用实心箭头 + //方法的参数依赖哪些Entity或ValueObject + addDependencyEdge(g, m); + //方法聚合了哪些实体 + methodAggregate(g, m); + }); + } + } + private void graphEntity(final AbilityGraph g, Set> entitys) { + if(entitys != null){ + for (Class entity : entitys){ + //领域实体的继承关系 + Class superClass = entity.getSuperclass(); + if(superClass != null && Entity.class.isAssignableFrom(superClass) && superClass != Entity.class){ + g.attribute.add(entity.getSimpleName() + AbilityGraph.BOX);//实体用矩形表示 + g.attribute.add(superClass.getSimpleName() + AbilityGraph.BOX);//父类实体用矩形表示 + g.edges.add(AbilityGraph.Edge.of(entity.getSimpleName(), superClass.getSimpleName()).type(AbilityGraph.Edge.TYPE.empty));//继承关系用空心箭头 + } + //领域实体的聚合关系 + Field[] fields = entity.getDeclaredFields(); + for (Field f : fields) { + //通过属性的依赖确定聚合关系 + fieldAggregate(g, f); + } + }; + } + } + private void graphService(final AbilityGraph g, Set> services) { + if(services != null){ + for (Class service : services){ + if(!service.isInterface()){ + continue; + } + //领域服务间的继承关系 + Class superClass = service.getSuperclass(); + if(superClass != null && ServiceI.class.isAssignableFrom(superClass) && superClass != ServiceI.class){ + g.edges.add(AbilityGraph.Edge.of(service.getSimpleName(), superClass.getSimpleName()).type(AbilityGraph.Edge.TYPE.empty));//继承关系用空心箭头 + } + //领域服务的方法都是领域的能力 + Method[] methods = service.getMethods(); + for (Method m : methods) { + String ability = m.getName(); + g.attribute.add(ability + AbilityGraph.POINT);//能力用点表示 + g.edges.add(AbilityGraph.Edge.of(ability, service.getSimpleName()).type(AbilityGraph.Edge.TYPE.normal).direct(AbilityGraph.Edge.DIR.back));//服务有哪些能力用实心箭头 + //方法的参数体现领域服务与领域Entity或ValueObject的依赖 + addDependencyEdge(g, m); + } + + }; + } + } + + /* + * 通过属性的依赖来体现聚合关系 + */ + private void fieldAggregate(final AbilityGraph g, Field f) { + Class rootClass = f.getDeclaringClass(); + String rootDomain = rootClass.getSimpleName(); + g.attribute.add(rootDomain + AbilityGraph.BOX);//根实体用矩形表示 + //属性的类型 + Type fieldType = f.getGenericType(); + addAggregateEdge(g, fieldType, rootClass); + } + /* + * 通过方法的依赖来体现聚合关系 + */ + private void methodAggregate(final AbilityGraph g, Method m) { + Class rootClass = m.getDeclaringClass(); + String rootDomain = rootClass.getSimpleName(); + g.attribute.add(rootDomain + AbilityGraph.BOX);//根实体用矩形表示 +// //方法参数类型 +// Type[] paramTypes = m.getGenericParameterTypes(); +// for (Type paramType : paramTypes) { +// addAggregateEdge(g, paramType, rootClass); +// } + //方法返回值类型 + Type returnType = m.getGenericReturnType(); + addAggregateEdge(g, returnType, rootClass); + + } + /* + * 添加依赖边 + */ + private void addDependencyEdge(final AbilityGraph g, Method m) { + String ability = m.getName(); + //方法参数依赖 + Type[] paramTypes = m.getGenericParameterTypes(); + for (Type p : paramTypes) { + List typeList = getClassT(p.getTypeName()); + for (String className : typeList) { + try { + Class pClass = Class.forName(className); + String domain = pClass.getSimpleName(); + if(Entity.class.isAssignableFrom(pClass)){ + g.attribute.add(domain + AbilityGraph.BOX);//实体用矩形表示 + g.edges.add(AbilityGraph.Edge.of(ability, domain).type(AbilityGraph.Edge.TYPE.normal).dottedLine());//参数依赖用虚线表示 + } + if(ValueObject.class.isAssignableFrom(pClass)){ + g.attribute.add(domain + AbilityGraph.DOTBOX);//值用虚线矩形表示 + g.edges.add(AbilityGraph.Edge.of(ability, domain).type(AbilityGraph.Edge.TYPE.normal).dottedLine());//参数依赖用虚线表示 + } + } catch (Exception e1) {} + } + } + } + /* + * 添加聚合边 + */ + private void addAggregateEdge(AbilityGraph g, Type domainType, Class rootDomainClass) { + String rootDomain = rootDomainClass.getSimpleName(); + List typeList = getClassT(domainType.getTypeName()); + for (String className : typeList) { + try { + Class domainClass = Class.forName(className); + String domain = domainClass.getSimpleName(); + if(Entity.class.isAssignableFrom(domainClass)){ + g.attribute.add(domain + AbilityGraph.BOX);//参数是实体,用矩形表示 + //如果有聚合关系,但是属性没有依赖,则把聚合关系飘红 + if(!isDependencyType(domainType, rootDomainClass.getDeclaredFields())){ + g.edges.add(AbilityGraph.Edge.of(domain, rootDomain).type(AbilityGraph.Edge.TYPE.diamond).color(AbilityGraph.Edge.COLOR.red)); + }else { + g.edges.add(AbilityGraph.Edge.of(domain, rootDomain).type(AbilityGraph.Edge.TYPE.diamond));//聚合关系用菱形表示 + } + } + if(ValueObject.class.isAssignableFrom(domainClass)){ + g.attribute.add(domain + AbilityGraph.DOTBOX);//参数是值对象,用虚线矩形表示 + //如果有聚合关系,但是属性没有依赖,则把聚合关系飘红 + if(!isDependencyType(domainType, rootDomainClass.getDeclaredFields())){ + g.edges.add(AbilityGraph.Edge.of(domain, rootDomain).type(AbilityGraph.Edge.TYPE.diamond).color(AbilityGraph.Edge.COLOR.red)); + }else { + g.edges.add(AbilityGraph.Edge.of(domain, rootDomain).type(AbilityGraph.Edge.TYPE.diamond));//聚合关系用菱形表示 + } + } + } catch (Exception e1) {} + } + } + + /** + * + * @param domainType + * @param fields + * @return + */ + private boolean isDependencyType(Type domainType, Field[] fields){ + if(fields == null){ + return false; + } + for (Field f : fields){ + if(f.getGenericType() == domainType){ + return true; + } + } + return false; + } + + /* + * 正则表达式匹配两个指定字符串中间的内容 + */ + private List getClassT(String soap){ + String regex = "<(.*?)>";//匹配泛型 + List list = new ArrayList(); + Pattern pattern = Pattern.compile(regex);// 匹配的模式 + Matcher m = pattern.matcher(soap); + while (m.find()) { + String match = m.group(1); + //有些泛型里带? extends 等需要过滤掉 + String[] s = match.split(" "); + list.add(s[s.length - 1]); + } + if(list.isEmpty()){ + list.add(soap); + } + return list; + } +} diff --git a/halo-collection/src/main/java/org/xujin/halo/domain/AbilityGraph.java b/halo-collection/src/main/java/org/xujin/halo/domain/AbilityGraph.java new file mode 100644 index 0000000..6a1a356 --- /dev/null +++ b/halo-collection/src/main/java/org/xujin/halo/domain/AbilityGraph.java @@ -0,0 +1,149 @@ +package org.xujin.halo.domain; + +import lombok.EqualsAndHashCode; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 系统能力图
+ * + * toString可生成标准dot语法,安装graphviz的机器可以通过命令dot -Tpng -o target.png inputfile生成能力图
+ * graphviz安装方法:
+ * ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
+ * brew install graphviz
+ * vim ~/.bash_profile
+ * export PATH=/usr/local/bin:$PATH
+ * source ~/.bash_profile
+ * + * dotguide + * + * @author xujin + * + */ +public class AbilityGraph { + + static String BOX = "[shape=box]"; + static String POINT = "[shape=plaintext]"; + static String DOTBOX = "[shape=box,style=dotted]"; + /** + * 有向图 + */ + String type = "digraph"; + String name = "G"; + /** + * 全局属性集合 + */ + Set attribute = new HashSet<>(); + /** + * 边集 + */ + Set edges = new HashSet<>(); + + private List subGraph = new ArrayList<>(); + + /** + * 加为子图 + * @param g + */ + public void addSubGraph(AbilityGraph g){ + g.type = "subgraph"; + g.name = this.name + subGraph.size(); + subGraph.add(g); + } + @Override + public String toString() { + //左右对齐 + attribute.add("rankdir=LR"); + StringBuffer sb = new StringBuffer(); + sb.append(type + " " + name + " {\n"); + attribute.forEach(attr -> { + sb.append(attr + "\n"); + }); + edges.forEach(e -> { + sb.append(e + "\n"); + }); + subGraph.forEach(sub -> { + sb.append(sub.toString()); + }); + sb.append("}"); + return sb.toString(); + } + /** + * 边 + */ + @EqualsAndHashCode(of={"x", "y"}) + public static class Edge { + /** + * 箭头方向:forward (the default), back, both, or none + * 箭头类型:normal, inv, dot, invdot, odot, invodot,none + */ + private List attr = new ArrayList<>(); + public static enum DIR{ + forward, back, both, none + } + public static enum TYPE{ + normal,dot,inv,crow,tee,vee,diamond,none,box,curve,icurve,empty + } + public static enum COLOR{ + red + } + + String x; + String y; + + public static Edge of(String x, String y){ + Edge e = new Edge(); + e.x = x; + e.y = y; + return e; + } + + /** + * 虚线 + * @return + */ + public Edge dottedLine(){ + attr.add("style=dotted"); + return this; + } + /** + * 箭头类型 + * @param type + * @return + */ + public Edge type(TYPE type){ + attr.add("arrowhead=" + type); + return this; + } + /** + * 箭头颜色 + * @param color + * @return + */ + public Edge color(COLOR color){ + attr.add("color=" + color); + return this; + } + /** + * 箭头方向 + * @param dir + * @return + */ + public Edge direct(DIR dir){ + attr.add("dir=" + dir); + return this; + } + @Override + public String toString() { + if(attr.isEmpty()){ + this.type(Edge.TYPE.normal); + } + StringBuffer sb = new StringBuffer(); + sb.append(x + " -> " + y + attr.toString()); + return sb.toString(); + } + } +} diff --git a/halo-collection/src/main/java/org/xujin/halo/domain/DomainUtils.java b/halo-collection/src/main/java/org/xujin/halo/domain/DomainUtils.java new file mode 100644 index 0000000..ba482ab --- /dev/null +++ b/halo-collection/src/main/java/org/xujin/halo/domain/DomainUtils.java @@ -0,0 +1,36 @@ +package org.xujin.halo.domain; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.Arrays; + +/** + * 领域模型工具 + */ +public class DomainUtils { + /** + * 生成领域模型图 + * @param packages + */ + public static void generationDomainGraph(String... packages){ + AbilityCollection ac = new AbilityCollection(); + Arrays.stream(packages).forEach(pkg -> { + ac.addPackage(pkg); + }); + AbilityGraph ag = ac.collectAbility(); + File file = new File("domain.dot"); + try(FileOutputStream out = new FileOutputStream(file)){ + file.createNewFile(); + //写入dotsource + out.write(ag.toString().getBytes()); + //执行dot命令生成png图片 + Runtime rt = Runtime.getRuntime(); + String[] param = {"dot", "-Tpng", "-o", "domain.png", "domain.dot"}; + Process p = rt.exec(param); + p.waitFor(); + System.out.println("Runtime process finish:" + String.join(" ", param)); + }catch(Exception e){ + e.printStackTrace(); + } + } +} diff --git a/halo-common/pom.xml b/halo-common/pom.xml new file mode 100644 index 0000000..cc3c875 --- /dev/null +++ b/halo-common/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + org.xujin.halo + halo + 1.0.4 + ../pom.xml + + halo-common + 1.0.4 + halo-common + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + + jar-no-fork + + + + + + + diff --git a/halo-common/src/main/java/org/xujin/halo/dto/ClientObject.java b/halo-common/src/main/java/org/xujin/halo/dto/ClientObject.java new file mode 100644 index 0000000..26c7722 --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/ClientObject.java @@ -0,0 +1,39 @@ +package org.xujin.halo.dto; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * This is the object communicate with Client. + * The clients could be view layer or other RPC Consumers + * @author xujin 2017-10-27 PM 12:19:15 + */ +public abstract class ClientObject extends DTO implements Serializable{ + + private static final long serialVersionUID = 1L; + + /** + * This is for extended values + */ + protected Map extValues = new HashMap(); + + public Object getExtField(String key){ + if(extValues != null){ + return extValues.get(key); + } + return null; + } + + public void putExtField(String fieldName, Object value){ + this.extValues.put(fieldName, value); + } + + public Map getExtValues() { + return extValues; + } + + public void setExtValues(Map extValues) { + this.extValues = extValues; + } +} diff --git a/halo-common/src/main/java/org/xujin/halo/dto/Command.java b/halo-common/src/main/java/org/xujin/halo/dto/Command.java new file mode 100644 index 0000000..4e8da4d --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/Command.java @@ -0,0 +1,38 @@ +package org.xujin.halo.dto; + +/** + * Command stands for a request from Client. + * According CommandExecutor will help to handle the business logic. This is a classic Command Pattern + * + * @author xujin + */ +public abstract class Command extends DTO{ + + private static final long serialVersionUID = 1L; + + /** + * 不需要操作人设置此值为true + */ + private boolean operaterIsNotNeeded = false; + + /** + * command的操作人 + */ + private String operater; + + public String getOperater() { + return operater; + } + + public void setOperater(String operater) { + this.operater = operater; + } + + public boolean isOperaterIsNotNeeded() { + return operaterIsNotNeeded; + } + + public void setOperaterIsNotNeeded(boolean operaterIsNotNeeded) { + this.operaterIsNotNeeded = operaterIsNotNeeded; + } +} diff --git a/halo-common/src/main/java/org/xujin/halo/dto/DTO.java b/halo-common/src/main/java/org/xujin/halo/dto/DTO.java new file mode 100644 index 0000000..0b2fd41 --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/DTO.java @@ -0,0 +1,16 @@ +package org.xujin.halo.dto; + +import java.io.Serializable; + +/** + * Data Transfer object, including Command, Query and Response, + * + * Command and Query is CQRS concept. + * + * @author xujin 2017年10月21日 下午8:53:55 + */ +public class DTO implements Serializable{ + + private static final long serialVersionUID = 1L; + +} diff --git a/halo-common/src/main/java/org/xujin/halo/dto/MultiResponse.java b/halo-common/src/main/java/org/xujin/halo/dto/MultiResponse.java new file mode 100644 index 0000000..fdd747e --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/MultiResponse.java @@ -0,0 +1,64 @@ +package org.xujin.halo.dto; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Response with batch record to return, + * usually use in page query or conditional query + *

+ * Created by xujin on 2018/6/1. + */ +public class MultiResponse extends Response { + + private int total; + + private Collection data; + + public static MultiResponse of(Collection data, int total) { + MultiResponse multiResponse = new MultiResponse<>(); + multiResponse.setSuccess(true); + multiResponse.setData(data); + multiResponse.setTotal(total); + return multiResponse; + } + + public static MultiResponse ofWithoutTotal(Collection data) { + return of(data,0); + } + + + public int getTotal() { + return total; + } + + + public void setTotal(int total) { + this.total = total; + } + + public List getData() { + return null == data ? new ArrayList<>() : new ArrayList<>(data); + } + + + public void setData(Collection data) { + this.data = data; + } + + public static MultiResponse buildFailure(String errCode, String errMessage) { + MultiResponse response = new MultiResponse(); + response.setSuccess(false); + response.setErrCode(errCode); + response.setErrMessage(errMessage); + return response; + } + + public static MultiResponse buildSuccess(){ + MultiResponse response = new MultiResponse(); + response.setSuccess(true); + return response; + } + +} diff --git a/halo-common/src/main/java/org/xujin/halo/dto/OrderDesc.java b/halo-common/src/main/java/org/xujin/halo/dto/OrderDesc.java new file mode 100644 index 0000000..82e97e8 --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/OrderDesc.java @@ -0,0 +1,28 @@ +package org.xujin.halo.dto; + +/** + * Order Description + * + * @author xujin 2018/5/19 + */ +public class OrderDesc { + + private String col; + private boolean asc = true; + + public String getCol() { + return col; + } + + public void setCol(String col) { + this.col = col; + } + + public boolean isAsc() { + return asc; + } + + public void setAsc(boolean asc) { + this.asc = asc; + } +} diff --git a/halo-common/src/main/java/org/xujin/halo/dto/PageQuery.java b/halo-common/src/main/java/org/xujin/halo/dto/PageQuery.java new file mode 100644 index 0000000..078ede6 --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/PageQuery.java @@ -0,0 +1,56 @@ +package org.xujin.halo.dto; + +import java.util.ArrayList; +import java.util.List; + +/** + * page query + *

+ * Created by xujin on 2018/6/1. + */ +public abstract class PageQuery extends Query { + + private int pageNum = 1; + private int pageSize = 10; + private boolean needTotalCount = true; + private List orderDescs; + + public int getPageNum() { + return pageNum; + } + + public void setPageNum(int pageNum) { + this.pageNum = pageNum; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public boolean isNeedTotalCount() { + return needTotalCount; + } + + public void setNeedTotalCount(boolean needTotalCount) { + this.needTotalCount = needTotalCount; + } + + public List getOrderDescs() { + return orderDescs; + } + + public void addOrderDesc(OrderDesc orderDesc) { + if (null == orderDescs) { + orderDescs = new ArrayList<>(); + } + orderDescs.add(orderDesc); + } + + public int getOffset() { + return pageNum > 0 ? (pageNum - 1) * pageSize : 0; + } +} diff --git a/halo-common/src/main/java/org/xujin/halo/dto/Query.java b/halo-common/src/main/java/org/xujin/halo/dto/Query.java new file mode 100644 index 0000000..7064b62 --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/Query.java @@ -0,0 +1,13 @@ +package org.xujin.halo.dto; + +/** + * + * Query is a special Command which will directly call DataTunnel for data objects + * + * @author xujin 2017年10月22日 下午7:26:49 + */ +public abstract class Query extends Command{ + + private static final long serialVersionUID = 1L; + +} diff --git a/halo-common/src/main/java/org/xujin/halo/dto/Response.java b/halo-common/src/main/java/org/xujin/halo/dto/Response.java new file mode 100644 index 0000000..57019c8 --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/Response.java @@ -0,0 +1,67 @@ +package org.xujin.halo.dto; + +/** + * Response to caller + * + * @author xujin 2017年10月21日 下午8:53:17 + */ +public class Response extends DTO{ + + private static final long serialVersionUID = 1L; + + private boolean isSuccess; + + private String errCode; + + private String errMessage; + + public boolean isSuccess() { + return isSuccess; + } + + + public void setSuccess(boolean isSuccess) { + this.isSuccess = isSuccess; + } + + + public String getErrCode() { + return errCode; + } + + + public void setErrCode(String errCode) { + this.errCode = errCode; + } + + + public String getErrMessage() { + return errMessage; + } + + + public void setErrMessage(String errMessage) { + this.errMessage = errMessage; + } + + + @Override + public String toString() { + return "Response [isSuccess=" + isSuccess + ", errCode=" + errCode + ", errMessage=" + errMessage + "]"; + } + + public static Response buildFailure(String errCode, String errMessage) { + Response response = new Response(); + response.setSuccess(false); + response.setErrCode(errCode); + response.setErrMessage(errMessage); + return response; + } + + public static Response buildSuccess(){ + Response response = new Response(); + response.setSuccess(true); + return response; + } + +} diff --git a/halo-common/src/main/java/org/xujin/halo/dto/SingleResponse.java b/halo-common/src/main/java/org/xujin/halo/dto/SingleResponse.java new file mode 100644 index 0000000..534eaae --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/SingleResponse.java @@ -0,0 +1,42 @@ +package org.xujin.halo.dto; + +/** + * Response with single record to return + *

+ * Created by xujin on 2018/6/1. + */ + +public class SingleResponse extends Response { + + private T data; + + public static SingleResponse of(T data) { + SingleResponse singleResponse = new SingleResponse<>(); + singleResponse.setSuccess(true); + singleResponse.setData(data); + return singleResponse; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + public static SingleResponse buildFailure(String errCode, String errMessage) { + SingleResponse response = new SingleResponse(); + response.setSuccess(false); + response.setErrCode(errCode); + response.setErrMessage(errMessage); + return response; + } + + public static SingleResponse buildSuccess(){ + SingleResponse response = new SingleResponse(); + response.setSuccess(true); + return response; + } + +} diff --git a/halo-common/src/main/java/org/xujin/halo/dto/event/Event.java b/halo-common/src/main/java/org/xujin/halo/dto/event/Event.java new file mode 100644 index 0000000..d92660b --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/event/Event.java @@ -0,0 +1,28 @@ +package org.xujin.halo.dto.event; + +import org.xujin.halo.dto.DTO; + +/** + * @author xujin + */ +public class Event extends DTO { + private static final long serialVersionUID = 5740150436439366761L; + protected String eventId; + protected String eventType; + + public String getEventType(){ + return eventType; + } + + public String getEventId(){ + return eventId; + } + + public void setEventId(String eventId) { + this.eventId = eventId; + } + + public void setEventType(String eventType) { + this.eventType = eventType; + } +} diff --git a/halo-common/src/main/java/org/xujin/halo/dto/event/EventType.java b/halo-common/src/main/java/org/xujin/halo/dto/event/EventType.java new file mode 100644 index 0000000..b928d62 --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/event/EventType.java @@ -0,0 +1,12 @@ +package org.xujin.halo.dto.event; + +/** + * @author xujin + * @date 2018/04/21 + */ +public enum EventType { + CREATE, + UPDATE, + DELETE + ; +} diff --git a/halo-common/src/main/java/org/xujin/halo/dto/event/MqEvent.java b/halo-common/src/main/java/org/xujin/halo/dto/event/MqEvent.java new file mode 100644 index 0000000..2d248b5 --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/dto/event/MqEvent.java @@ -0,0 +1,12 @@ +package org.xujin.halo.dto.event; + +/** + * + * @author xujin + * @date 2018/05/17 + */ +public abstract class MqEvent extends Event { + + public abstract String getTopic(); + +} diff --git a/halo-common/src/main/java/org/xujin/halo/method/MethodExecutor.java b/halo-common/src/main/java/org/xujin/halo/method/MethodExecutor.java new file mode 100644 index 0000000..a56ea6b --- /dev/null +++ b/halo-common/src/main/java/org/xujin/halo/method/MethodExecutor.java @@ -0,0 +1,47 @@ +package org.xujin.halo.method; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * 方法执行器 + */ +public abstract class MethodExecutor { + // 目标方法 + private Method targetMethod; + + public MethodExecutor(Method targetMethod) { + this.targetMethod = targetMethod; + } + + /** + * 执行方法 + * + * @param obj 被执行的对象 + * @param args 需传入目标方法的参数 + * @return 目标方法返回的结果 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + protected Object execute(Object obj, Object[] args) throws Throwable { + try { + return targetMethod.invoke(obj, args); + } catch (InvocationTargetException e) { + // 抛出原始异常 + throw e.getTargetException(); + } + } + + /** + * 获取目标方法入参类型 + */ + public Class[] getParameterTypes() { + return targetMethod.getParameterTypes(); + } + + /** + * 获取目标方法返回类型 + */ + public Class getReturnType() { + return targetMethod.getReturnType(); + } +} diff --git a/halo-core/pom.xml b/halo-core/pom.xml new file mode 100644 index 0000000..301fd3a --- /dev/null +++ b/halo-core/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + org.xujin.halo + halo + 1.0.4 + ../pom.xml + + halo-core + 1.0.4 + halo-core + + + + org.xujin.halo + halo-base + + + + org.projectlombok + lombok + + + org.springframework + spring-core + + + org.springframework + spring-context + + + org.slf4j + slf4j-api + + + org.reflections + reflections + + + org.apache.commons + commons-lang3 + + + + org.hibernate.validator + hibernate-validator + + + javax.el + javax.el-api + test + + + org.glassfish.web + javax.el + test + + + + junit + junit + test + + + org.springframework + spring-test + test + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + + jar-no-fork + + + + + + + diff --git a/halo-core/src/main/java/org/xujin/halo/assembler/AntiCorruptionI.java b/halo-core/src/main/java/org/xujin/halo/assembler/AntiCorruptionI.java new file mode 100644 index 0000000..d64701a --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/assembler/AntiCorruptionI.java @@ -0,0 +1,10 @@ +package org.xujin.halo.assembler; + +/** + * 防腐层 + * 适用于消息,查询,RPC等接口的参数装配 + * @author xujin + * @date 2018/04/27 + */ +public interface AntiCorruptionI { +} diff --git a/halo-core/src/main/java/org/xujin/halo/assembler/AssemblerI.java b/halo-core/src/main/java/org/xujin/halo/assembler/AssemblerI.java new file mode 100644 index 0000000..0d81b60 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/assembler/AssemblerI.java @@ -0,0 +1,25 @@ +package org.xujin.halo.assembler; + + +/** + * 适用于消息,查询,RPC等接口的参数装配 + * Assembler Interface + * + * @author xujin + */ +public interface AssemblerI extends AntiCorruptionI{ + + default T assembleParam(F from) { + return null; + } + + default void assembleParam(F from, T to) { + } + + default T assembleResult(F from) { + return null; + } + + default void assembleResult(F from, T to) { + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/AbstractRegister.java b/halo-core/src/main/java/org/xujin/halo/boot/AbstractRegister.java new file mode 100644 index 0000000..564bac0 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/AbstractRegister.java @@ -0,0 +1,69 @@ +package org.xujin.halo.boot; + +import org.xujin.halo.exception.InfraException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.util.List; + +/** + * @author xujin + * @date 2018/02/13 + */ +public abstract class AbstractRegister implements RegisterI{ + + protected ApplicationContext applicationContext; + + protected T getBean(Class targetClz){ + T executorI = null; + //优先按type查 + try { + executorI = (T) applicationContext.getBean(targetClz); + }catch (Exception e){ + } + //按name查 + if(executorI == null){ + String simpleName = targetClz.getSimpleName(); + //首字母小写 + simpleName = Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1); + executorI = (T) applicationContext.getBean(simpleName); + } + if(executorI == null){ + new InfraException("Command " + targetClz + " init error!"); + } + return executorI; + } + + /** + * 根据Order注解排序 + * @param interceptorIList + */ + protected void order(List interceptorIList){ + if(interceptorIList == null || interceptorIList.size() <= 1){ + return; + } + T newInterceptor = interceptorIList.get(interceptorIList.size() - 1); + Order order = newInterceptor.getClass().getDeclaredAnnotation(Order.class); + if(order == null){ + return; + } + int index = interceptorIList.size() - 1; + for(int i = interceptorIList.size() - 2; i >= 0; i--){ + int itemOrderInt = Ordered.LOWEST_PRECEDENCE; + Order itemOrder = interceptorIList.get(i).getClass().getDeclaredAnnotation(Order.class); + if(itemOrder != null){ + itemOrderInt = itemOrder.value(); + } + if(itemOrderInt > order.value()){ + interceptorIList.set(index, interceptorIList.get(i)); + index = i; + }else { + break; + } + } + if(index < interceptorIList.size() - 1){ + interceptorIList.set(index, newInterceptor); + } + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/Bootstrap.java b/halo-core/src/main/java/org/xujin/halo/boot/Bootstrap.java new file mode 100644 index 0000000..b017f4c --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/Bootstrap.java @@ -0,0 +1,67 @@ +package org.xujin.halo.boot; + +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.springframework.beans.factory.annotation.Autowired; + +import org.xujin.halo.exception.InfraException; + +import lombok.Getter; +import lombok.Setter; + +/** + * 应用的核心引导启动类 + *

+ * 负责扫描在applicationContext.xml中配置的packages. 获取到CommandExecutors, intercepters, extensions, validators等 + * 交给各个注册器进行注册。 + * + * @author xujin 2017-11-04 + */ +public class Bootstrap { + @Getter + @Setter + private List packages; + private ClassPathScanHandler handler; + + @Autowired + private RegisterFactory registerFactory; + + + public void init() { + Set> classSet = scanConfiguredPackages(); + registerBeans(classSet); + } + + /** + * @param classSet + */ + private void registerBeans(Set> classSet) { + for (Class targetClz : classSet) { + RegisterI register = registerFactory.getRegister(targetClz); + if (null != register) { + register.doRegistration(targetClz); + } + } + + } + + /** + * Scan the packages configured in Spring xml + * + * @return + */ + private Set> scanConfiguredPackages() { + if (packages == null) throw new InfraException("Command packages is not specified"); + + String[] pkgs = new String[packages.size()]; + handler = new ClassPathScanHandler(packages.toArray(pkgs)); + + Set> classSet = new TreeSet<>(new ClassNameComparator()); + for (String pakName : packages) { + classSet.addAll(handler.getPackageAllClasses(pakName, true)); + } + return classSet; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/ClassInterfaceChecker.java b/halo-core/src/main/java/org/xujin/halo/boot/ClassInterfaceChecker.java new file mode 100644 index 0000000..4e2efc0 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/ClassInterfaceChecker.java @@ -0,0 +1,28 @@ +package org.xujin.halo.boot; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +/** + * @author xujin + * @date 2018/3/21 + */ +public class ClassInterfaceChecker { + + public static boolean check(Class targetClz, String expectedName){ + //If it is not Class, just return false + if(targetClz.isInterface()){ + return false; + } + + Class[] interfaces = targetClz.getInterfaces(); + if (ArrayUtils.isEmpty(interfaces)) + return false; + for (Class intf : interfaces) { + String intfSimpleName = intf.getSimpleName(); + if (StringUtils.equals(intfSimpleName, expectedName)) + return true; + } + return false; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/ClassNameComparator.java b/halo-core/src/main/java/org/xujin/halo/boot/ClassNameComparator.java new file mode 100644 index 0000000..c2bc6df --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/ClassNameComparator.java @@ -0,0 +1,14 @@ +package org.xujin.halo.boot; + +import java.util.Comparator; + +public class ClassNameComparator implements Comparator> { + @Override + public int compare(Class o1, Class o2) { + if (o1 == null) + return -1; + if (o2 == null) + return 1; + return o1.getName().compareToIgnoreCase(o2.getName()); + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/ClassPathScanHandler.java b/halo-core/src/main/java/org/xujin/halo/boot/ClassPathScanHandler.java new file mode 100644 index 0000000..4fd863b --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/ClassPathScanHandler.java @@ -0,0 +1,303 @@ +package org.xujin.halo.boot; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLDecoder; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Pattern; + +import javax.annotation.Nonnull; + +import org.reflections.Reflections; +import org.reflections.scanners.SubTypesScanner; +import org.reflections.scanners.TypeAnnotationsScanner; +import org.reflections.util.ConfigurationBuilder; + +import org.xujin.halo.logger.Logger; +import org.xujin.halo.logger.LoggerFactory; + +import lombok.Getter; +import lombok.Setter; + + +/** + * @author xujin + * @since 15/7/25 + */ +public class ClassPathScanHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(ClassPathScanHandler.class); + + /** + * class file extension name. + */ + private static final String CLASS_EXTENSION_NAME = ".class"; + /** + * 是否排除内部类 true->是 false->否. + */ + @Getter + @Setter + private boolean excludeInner = true; + /** + * 过滤规则适用情况 true—>搜索符合规则的 false->排除符合规则的. + */ + @Getter + @Setter + private boolean checkInOrEx = true; + /** + * 过滤规则列表 如果是null或者空,即全部符合不过滤. + */ + @Getter + @Setter + private List classFilters = null; + /** + * the reflections. + */ + @Getter + @Setter + private Reflections reflections = null; + + /** + * the classes and the packages to be scanned. + * + * @param packages packages. + */ + public ClassPathScanHandler(String... packages) { + this.reflections = new Reflections(new ConfigurationBuilder(). + forPackages(packages). + addScanners(new TypeAnnotationsScanner(), new SubTypesScanner())//.addUrls(urlList) + ); + } + + /** + * excludeInner:是否排除内部类 true->是 false->否
. + * checkInOrEx:过滤规则适用情况 true—>搜索符合规则的 false->排除符合规则的
+ * classFilters:自定义过滤规则,如果是null或者空,即全部符合不过滤 + * + * @param excludeInner whether exclude the inner class. + * @param checkInOrEx whether exclude the rule checking. + * @param classFilters the customized the classes to be filtered. + */ + public ClassPathScanHandler(Boolean excludeInner, Boolean checkInOrEx, + List classFilters) { + this.excludeInner = excludeInner; + this.checkInOrEx = checkInOrEx; + this.classFilters = classFilters; + + } + + /** + * get all the classes with annotation. + * + * @param annotation the specific annotation. + * @param honorInherited honorInherited + * @return the set of the classes. + */ + public Set> getAllClassesWithAnnotation(Class annotation, boolean honorInherited) { + return reflections.getTypesAnnotatedWith(annotation, honorInherited); + } + + /** + * get all the sub classes with the specific parent class. + * + * @param parent the parent class. + * @param sub class's type. + * @return the set of the sub classes found. + */ + public Set> getAllSubClassesByParent(Class parent) { + return reflections.getSubTypesOf(parent); + } + + /** + * scan the package. + * + * @param basePackage the basic class package's string. + * @param recursive whether to search recursive. + * @return Set of the found classes. + */ + public Set> getPackageAllClasses(String basePackage, boolean recursive) { + if (basePackage == null) + return new HashSet<>(); + Set> classes = new LinkedHashSet>(); + String packageName = basePackage; + if (packageName.endsWith(".")) { + packageName = packageName.substring(0, packageName.lastIndexOf('.')); + } + String package2Path = packageName.replace('.', '/'); + + Enumeration dirs; + try { + dirs = Thread.currentThread().getContextClassLoader().getResources(package2Path); + while (dirs.hasMoreElements()) { + URL url = dirs.nextElement(); + String protocol = url.getProtocol(); + if ("file".equals(protocol)) { + LOGGER.debug("扫描file类型的class文件...."); + String filePath = URLDecoder.decode(url.getFile(), "UTF-8"); + doScanPackageClassesByFile(classes, packageName, filePath, recursive); + } else if ("jar".equals(protocol)) { + LOGGER.debug("扫描jar文件中的类...."); + doScanPackageClassesByJar(packageName, url, recursive, classes); + } + } + } catch (IOException e) { + LOGGER.error("IOException error:"); + } + + TreeSet> sortedClasses = new TreeSet<>(new ClassNameComparator()); + sortedClasses.addAll(classes); + return sortedClasses; + } + + /** + * 以jar的方式扫描包下的所有Class文件
. + * + * @param basePackage eg:michael.utils. + * @param url the url. + * @param recursive whether to search recursive. + * @param classes set of the found classes. + */ + private void doScanPackageClassesByJar(String basePackage, URL url, final boolean recursive, Set> classes) { + String package2Path = basePackage.replace('.', '/'); + JarFile jar; + try { + jar = ((JarURLConnection) url.openConnection()).getJarFile(); + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + if (!name.startsWith(package2Path) || entry.isDirectory()) { + continue; + } + // 判断是否递归搜索子包 + if (!recursive && name.lastIndexOf('/') != package2Path.length()) { + continue; + } + // 判断是否过滤 inner class + if (this.excludeInner && name.indexOf('$') != -1) { + LOGGER.debug("exclude inner class with name:" + name); + continue; + } + String classSimpleName = name.substring(name.lastIndexOf('/') + 1); + // 判定是否符合过滤条件 + if (this.filterClassName(classSimpleName)) { + String className = name.replace('/', '.'); + className = className.substring(0, className.length() - 6); + try { + classes.add(Thread.currentThread().getContextClassLoader().loadClass(className)); + } catch (ClassNotFoundException e) { + LOGGER.error("Class.forName error:URL is ===>" + url.getPath()); + } + } + } + } catch (IOException e) { + LOGGER.error("IOException error:URL is ===>" + url.getPath()); + } catch (Throwable e) { + LOGGER.error("ScanPackageClassesByJar error:URL is ===>" + url.getPath()); + } + } + + /** + * 以文件的方式扫描包下的所有Class文件. + * + * @param packageName the package name for scanning. + * @param packagePath the package path for scanning. + * @param recursive whether to search recursive. + * @param classes set of the found classes. + */ + private void doScanPackageClassesByFile( + Set> classes, String packageName, String packagePath, final boolean recursive) { + File dir = new File(packagePath); + if (!dir.exists() || !dir.isDirectory()) { + return; + } + File[] files = dir.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return filterClassFileByCustomization(pathname, recursive); + } + }); + + if (null == files || files.length == 0) { + return; + } + for (File file : files) { + if (file.isDirectory()) { + doScanPackageClassesByFile(classes, packageName + "." + file.getName(), file.getAbsolutePath(), recursive); + } else { + String className = file.getName().substring(0, + file.getName().length() - CLASS_EXTENSION_NAME.length()); + try { + classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className)); + } catch (ClassNotFoundException e) { + LOGGER.error("IOException error:"); + } + } + } + } + + /** + * filter the class file from the customized rules. + * + * @param file the class file to be filtered. + * @param recursive whether search recursive. + * @return true: match, false: not match. + */ + private boolean filterClassFileByCustomization(@Nonnull File file, boolean recursive) { + if (file.isDirectory()) { + return recursive; + } + String filename = file.getName(); + if (excludeInner && filename.indexOf('$') != -1) { + LOGGER.debug("exclude inner class with name:" + filename); + return false; + } + return filterClassName(filename); + } + + /** + * 根据过滤规则判断类名. + * + * @param className the class name. + * @return whether to be filtered. + */ + private boolean filterClassName(String className) { + if (!className.endsWith(CLASS_EXTENSION_NAME)) { + return false; + } + if (null == this.classFilters || this.classFilters.isEmpty()) { + return true; + } + String tmpName = className.substring(0, className.length() - 6); + boolean flag = false; + for (String str : classFilters) { + flag = matchInnerClassname(tmpName, str); + if (flag) break; + } + return (checkInOrEx && flag) || (!checkInOrEx && !flag); + } + + /** + * check the className whether match the inner class's rule. + * + * @param className the inner class name. + * @param filterString the filter string. + * @return true or false. + */ + private boolean matchInnerClassname(String className, String filterString) { + String reg = "^" + filterString.replace("*", ".*") + "$"; + Pattern p = Pattern.compile(reg); + return p.matcher(className).find(); + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/CommandRegister.java b/halo-core/src/main/java/org/xujin/halo/boot/CommandRegister.java new file mode 100644 index 0000000..f820a01 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/CommandRegister.java @@ -0,0 +1,83 @@ +package org.xujin.halo.boot; + +import org.xujin.halo.command.CommandHub; +import org.xujin.halo.command.CommandInterceptorI; +import org.xujin.halo.command.CommandInvocation; +import org.xujin.halo.common.CoreConstant; +import org.xujin.halo.dto.Command; +import org.xujin.halo.exception.InfraException; +import com.google.common.collect.Iterables; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * CommandRegister + * + * @author xujin 2017-11-04 + */ + +@SuppressWarnings({ "rawtypes", "unchecked" }) +@Component +public class CommandRegister extends AbstractRegister implements ApplicationContextAware { + + @Autowired + private CommandHub commandHub; + + @Override + public void doRegistration(Class targetClz) { + Class commandClz = getCommandFromExecutor(targetClz); + CommandInvocation commandInvocation = new CommandInvocation(); + + commandInvocation.setCommandExecutor(getBean(targetClz)); + commandInvocation.setPreInterceptors(collectInterceptors(commandClz, true)); + commandInvocation.setPostInterceptors(collectInterceptors(commandClz, false)); + commandHub.getCommandRepository().put(commandClz, commandInvocation); + } + + private Class getCommandFromExecutor(Class commandExecutorClz) { + Method[] methods = commandExecutorClz.getDeclaredMethods(); + for (Method method : methods) { + Class[] exeParams = method.getParameterTypes(); + /** + * This is for return right response type on exception scenarios + */ + if (CoreConstant.EXE_METHOD.equals(method.getName()) && exeParams.length == 1 + && Command.class.isAssignableFrom(exeParams[0]) && !method.isBridge()) { + commandHub.getResponseRepository().put(exeParams[0], method.getReturnType()); + return (Class) exeParams[0]; + } + } + throw new InfraException("Command param in " + commandExecutorClz + " " + CoreConstant.EXE_METHOD + + "() is not detected"); + } + + private Iterable collectInterceptors(Class commandClass, boolean pre) { + /** + * add 通用的Interceptors + */ + Iterable commandItr = Iterables.concat((pre ? commandHub.getGlobalPreInterceptors() : commandHub.getGlobalPostInterceptors())); + /** + * add command自己专属的Interceptors + */ + Iterables.concat(commandItr, (pre ? commandHub.getPreInterceptors() : commandHub.getPostInterceptors()).get(commandClass)); + /** + * add parents的Interceptors + */ + Class superClass = commandClass.getSuperclass(); + while (Command.class.isAssignableFrom(superClass)) { + Iterables.concat(commandItr, (pre ? commandHub.getPreInterceptors() : commandHub.getPostInterceptors()).get(commandClass)); + superClass = superClass.getSuperclass(); + } + return commandItr; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/ComponentExecutor.java b/halo-core/src/main/java/org/xujin/halo/boot/ComponentExecutor.java new file mode 100644 index 0000000..455b604 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/ComponentExecutor.java @@ -0,0 +1,17 @@ +package org.xujin.halo.boot; + +import java.util.function.Function; + +/** + * @author xujin + * @date 2018/4/21 + */ +public abstract class ComponentExecutor { + + public R execute(Class targetClz, Function exeFunction) { + C component = locateComponent(targetClz); + return exeFunction.apply(component); + } + + protected abstract C locateComponent(Class targetClz); +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/EventRegister.java b/halo-core/src/main/java/org/xujin/halo/boot/EventRegister.java new file mode 100644 index 0000000..69564c8 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/EventRegister.java @@ -0,0 +1,57 @@ +package org.xujin.halo.boot; + +import org.xujin.halo.common.CoreConstant; +import org.xujin.halo.dto.event.Event; +import org.xujin.halo.event.EventHandlerI; +import org.xujin.halo.event.EventHub; +import org.xujin.halo.exception.InfraException; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * EventRegister + * + * @author xujin + * @date 2018/3/20 + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +@Component +public class EventRegister extends AbstractRegister implements ApplicationContextAware { + + @Autowired + private EventHub eventHub; + + @Override + public void doRegistration(Class targetClz) { + Class eventClz = getEventFromExecutor(targetClz); + EventHandlerI executor = getBean(targetClz); + eventHub.register(eventClz, executor); + } + + private Class getEventFromExecutor(Class eventExecutorClz) { + Method[] methods = eventExecutorClz.getDeclaredMethods(); + for (Method method : methods) { + Class[] exeParams = method.getParameterTypes(); + /** + * This is for return right response type on exception scenarios + */ + if (CoreConstant.EXE_METHOD.equals(method.getName()) && exeParams.length == 1 + && Event.class.isAssignableFrom(exeParams[0]) && !method.isBridge()) { + eventHub.getResponseRepository().put(eventExecutorClz, method.getReturnType()); + return (Class) exeParams[0]; + } + } + throw new InfraException("Event param in " + eventExecutorClz + " " + CoreConstant.EXE_METHOD + + "() is not detected"); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/ExtensionRegister.java b/halo-core/src/main/java/org/xujin/halo/boot/ExtensionRegister.java new file mode 100644 index 0000000..9daf3b6 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/ExtensionRegister.java @@ -0,0 +1,63 @@ +package org.xujin.halo.boot; + +import org.xujin.halo.common.CoreConstant; +import org.xujin.halo.exception.InfraException; +import org.xujin.halo.extension.Extension; +import org.xujin.halo.extension.ExtensionCoordinate; +import org.xujin.halo.extension.ExtensionPointI; +import org.xujin.halo.extension.ExtensionRepository; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * ExtensionRegister + * @author xujin + */ +@Component +public class ExtensionRegister implements RegisterI, ApplicationContextAware{ + + @Autowired + private ExtensionRepository extensionRepository; + + private ApplicationContext applicationContext; + + @Override + public void doRegistration(Class targetClz) { + ExtensionPointI extension = (ExtensionPointI) applicationContext.getBean(targetClz); + Extension extensionAnn = targetClz.getDeclaredAnnotation(Extension.class); + String extensionPoint = calculateExtensionPoint(targetClz); + ExtensionCoordinate extensionCoordinate = new ExtensionCoordinate(extensionPoint, extensionAnn.bizCode(), extensionAnn.tenantId()); + ExtensionPointI preVal = extensionRepository.getExtensionRepo().put(extensionCoordinate, extension); + if (preVal != null) { + throw new InfraException("Duplicate registration is not allowed for :"+extensionCoordinate); + } + } + + /** + * @param targetClz + * @return + */ + private String calculateExtensionPoint(Class targetClz) { + Class[] interfaces = targetClz.getInterfaces(); + if (ArrayUtils.isEmpty(interfaces)) + throw new InfraException("Please assign a extension point interface for "+targetClz); + for (Class intf : interfaces) { + String extensionPoint = intf.getSimpleName(); + if (StringUtils.contains(extensionPoint, CoreConstant.EXTENSION_EXTPT_NAMING)) + return extensionPoint; + } + throw new InfraException("Your name of ExtensionPoint for "+targetClz+" is not valid, must be end of "+CoreConstant.EXTENSION_EXTPT_NAMING); + } + + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} \ No newline at end of file diff --git a/halo-core/src/main/java/org/xujin/halo/boot/PlainRuleRegister.java b/halo-core/src/main/java/org/xujin/halo/boot/PlainRuleRegister.java new file mode 100644 index 0000000..df1e921 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/PlainRuleRegister.java @@ -0,0 +1,34 @@ +package org.xujin.halo.boot; + +import org.xujin.halo.rule.PlainRuleRepository; +import org.xujin.halo.rule.RuleI; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * Plain Rule is Rule Component without ExtensionPoint + * @author xujin + * @date 2018/6/21 + */ +@Component +public class PlainRuleRegister implements RegisterI, ApplicationContextAware { + + @Autowired + private PlainRuleRepository plainRuleRepository; + + private ApplicationContext applicationContext; + + @Override + public void doRegistration(Class targetClz) { + RuleI plainRule = (RuleI) applicationContext.getBean(targetClz); + plainRuleRepository.getPlainRules().put(plainRule.getClass(), plainRule); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/PlainValidatorRegister.java b/halo-core/src/main/java/org/xujin/halo/boot/PlainValidatorRegister.java new file mode 100644 index 0000000..3ff05e8 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/PlainValidatorRegister.java @@ -0,0 +1,33 @@ +package org.xujin.halo.boot; + +import org.xujin.halo.validator.PlainValidatorRepository; +import org.xujin.halo.validator.ValidatorI; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * Plain Validator is Validator Component without ExtensionPoint + * @author xujin + */ +@Component +public class PlainValidatorRegister implements RegisterI, ApplicationContextAware { + + @Autowired + private PlainValidatorRepository plainValidatorRepository; + + private ApplicationContext applicationContext; + + @Override + public void doRegistration(Class targetClz) { + ValidatorI plainValidator= (ValidatorI) applicationContext.getBean(targetClz); + plainValidatorRepository.getPlainValidators().put(plainValidator.getClass(), plainValidator); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/PostInterceptorRegister.java b/halo-core/src/main/java/org/xujin/halo/boot/PostInterceptorRegister.java new file mode 100644 index 0000000..e9806aa --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/PostInterceptorRegister.java @@ -0,0 +1,48 @@ + +package org.xujin.halo.boot; + +import org.xujin.halo.command.CommandHub; +import org.xujin.halo.command.CommandInterceptorI; +import org.xujin.halo.command.PostInterceptor; +import org.xujin.halo.dto.Command; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * PostInterceptorRegister + * @author xujin + */ +@Component +public class PostInterceptorRegister extends AbstractRegister implements ApplicationContextAware{ + + @Autowired + private CommandHub commandHub; + + @Override + public void doRegistration(Class targetClz) { + CommandInterceptorI commandInterceptor = getBean(targetClz); + PostInterceptor postInterceptorAnn = targetClz.getDeclaredAnnotation(PostInterceptor.class); + Class[] supportClasses = postInterceptorAnn.commands(); + registerInterceptor(supportClasses, commandInterceptor); + } + + private void registerInterceptor(Class[] supportClasses, CommandInterceptorI commandInterceptor) { + if (null == supportClasses || supportClasses.length == 0) { + commandHub.getGlobalPostInterceptors().add(commandInterceptor); + order(commandHub.getGlobalPostInterceptors()); + return; + } + for (Class supportClass : supportClasses) { + commandHub.getPostInterceptors().put(supportClass, commandInterceptor); + order(commandHub.getPostInterceptors().get(supportClass)); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/PreInterceptorRegister.java b/halo-core/src/main/java/org/xujin/halo/boot/PreInterceptorRegister.java new file mode 100644 index 0000000..c82558a --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/PreInterceptorRegister.java @@ -0,0 +1,48 @@ + +package org.xujin.halo.boot; + +import org.xujin.halo.command.CommandHub; +import org.xujin.halo.command.CommandInterceptorI; +import org.xujin.halo.command.PreInterceptor; +import org.xujin.halo.dto.Command; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * PreInterceptorRegister + * @author xujin + */ +@Component +public class PreInterceptorRegister extends AbstractRegister implements ApplicationContextAware{ + + @Autowired + private CommandHub commandHub; + + @Override + public void doRegistration(Class targetClz) { + CommandInterceptorI commandInterceptor = getBean(targetClz); + PreInterceptor preInterceptorAnn = targetClz.getDeclaredAnnotation(PreInterceptor.class); + Class[] supportClasses = preInterceptorAnn.commands(); + registerInterceptor(supportClasses, commandInterceptor); + } + + private void registerInterceptor(Class[] supportClasses, CommandInterceptorI commandInterceptor) { + if (null == supportClasses || supportClasses.length == 0) { + commandHub.getGlobalPreInterceptors().add(commandInterceptor); + order(commandHub.getGlobalPreInterceptors()); + return; + } + for (Class supportClass : supportClasses) { + commandHub.getPreInterceptors().put(supportClass, commandInterceptor); + order(commandHub.getPreInterceptors().get(supportClass)); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/RegisterFactory.java b/halo-core/src/main/java/org/xujin/halo/boot/RegisterFactory.java new file mode 100644 index 0000000..20ebbab --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/RegisterFactory.java @@ -0,0 +1,88 @@ + +package org.xujin.halo.boot; + +import org.xujin.halo.command.Command; +import org.xujin.halo.command.PostInterceptor; +import org.xujin.halo.command.PreInterceptor; +import org.xujin.halo.common.CoreConstant; +import org.xujin.halo.event.EventHandler; +import org.xujin.halo.exception.InfraException; +import org.xujin.halo.extension.Extension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * RegisterFactory + * + * @author xujin + */ +@Component +public class RegisterFactory{ + + @Autowired + private PreInterceptorRegister preInterceptorRegister; + @Autowired + private PostInterceptorRegister postInterceptorRegister; + @Autowired + private CommandRegister commandRegister; + @Autowired + private ExtensionRegister extensionRegister; + @Autowired + private EventRegister eventRegister; + @Autowired + private PlainValidatorRegister plainValidatorRegister; + @Autowired + private PlainRuleRegister plainRuleRegister; + + public RegisterI getRegister(Class targetClz) { + PreInterceptor preInterceptorAnn = targetClz.getDeclaredAnnotation(PreInterceptor.class); + if (preInterceptorAnn != null) { + return preInterceptorRegister; + } + PostInterceptor postInterceptorAnn = targetClz.getDeclaredAnnotation(PostInterceptor.class); + if (postInterceptorAnn != null) { + return postInterceptorRegister; + } + Command commandAnn = targetClz.getDeclaredAnnotation(Command.class); + if (commandAnn != null) { + return commandRegister; + } + Extension extensionAnn = targetClz.getDeclaredAnnotation(Extension.class); + if (extensionAnn != null) { + return extensionRegister; + } + if (isPlainValidator(targetClz)) { + return plainValidatorRegister; + } + if (isPlainRule(targetClz)) { + return plainRuleRegister; + } + EventHandler eventHandlerAnn = targetClz.getDeclaredAnnotation(EventHandler.class); + if (eventHandlerAnn != null) { + return eventRegister; + } + return null; + } + + private boolean isPlainRule(Class targetClz) { + if (ClassInterfaceChecker.check(targetClz, CoreConstant.RULEI_CLASS) && makeSureItsNotExtensionPoint(targetClz)) { + return true; + } + return false; + } + + private boolean isPlainValidator(Class targetClz) { + if (ClassInterfaceChecker.check(targetClz, CoreConstant.VALIDATORI_CLASS) && makeSureItsNotExtensionPoint(targetClz)) { + return true; + } + return false; + } + + private boolean makeSureItsNotExtensionPoint(Class targetClz) { + if (ClassInterfaceChecker.check(targetClz, CoreConstant.EXTENSIONPOINT_CLASS)) { + throw new InfraException( + "Please add @Extension for " + targetClz.getSimpleName() + " since it's a ExtensionPoint"); + } + return true; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/boot/RegisterI.java b/halo-core/src/main/java/org/xujin/halo/boot/RegisterI.java new file mode 100644 index 0000000..8b71eeb --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/boot/RegisterI.java @@ -0,0 +1,11 @@ + +package org.xujin.halo.boot; + + +/** + * Register Interface + * @author xujin + */ +public interface RegisterI { + public void doRegistration(Class targetClz); +} diff --git a/halo-core/src/main/java/org/xujin/halo/command/Command.java b/halo-core/src/main/java/org/xujin/halo/command/Command.java new file mode 100644 index 0000000..5e90063 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/command/Command.java @@ -0,0 +1,17 @@ +package org.xujin.halo.command; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Component +public @interface Command { + +} diff --git a/halo-core/src/main/java/org/xujin/halo/command/CommandBus.java b/halo-core/src/main/java/org/xujin/halo/command/CommandBus.java new file mode 100644 index 0000000..9cc10dc --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/command/CommandBus.java @@ -0,0 +1,67 @@ +package org.xujin.halo.command; + +import org.xujin.halo.exception.BasicErrorCode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.xujin.halo.context.TenantContext; +import org.xujin.halo.dto.Command; +import org.xujin.halo.dto.Response; +import org.xujin.halo.exception.HaloException; +import org.xujin.halo.exception.ErrorCodeI; +import org.xujin.halo.exception.InfraException; +import org.xujin.halo.logger.Logger; +import org.xujin.halo.logger.LoggerFactory; + +/** + * Just send Command to CommandBus, + * + * @author xujin 2017年10月24日 上午12:47:18 + */ +@Component +public class CommandBus implements CommandBusI{ + + Logger logger = LoggerFactory.getLogger(CommandBus.class); + + @Autowired + private CommandHub commandHub; + + @SuppressWarnings("unchecked") + @Override + public Response send(Command cmd) { + Response response = null; + try { + //从commandHub中获取到对应的命令去调用命令的execute方法 + response = commandHub.getCommandInvocation(cmd.getClass()).invoke(cmd); + } + catch (Exception exception) { + //统一的Command异常处理器 + response = handleException(cmd, response, exception); + } + finally { + //Clean up context + TenantContext.remove(); + } + return response; + } + + private Response handleException(Command cmd, Response response, Exception exception) { + logger.error(exception.getMessage(), exception); + Class responseClz = commandHub.getResponseRepository().get(cmd.getClass()); + try { + response = (Response) responseClz.newInstance(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + throw new InfraException(e.getMessage()); + } + if (exception instanceof HaloException) { + ErrorCodeI errCode = ((HaloException) exception).getErrCode(); + response.setErrCode(errCode.getErrCode()); + } + else { + response.setErrCode(BasicErrorCode.SYS_ERROR.getErrCode()); + } + response.setErrMessage(exception.getMessage()); + return response; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/command/CommandBusI.java b/halo-core/src/main/java/org/xujin/halo/command/CommandBusI.java new file mode 100644 index 0000000..2015d42 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/command/CommandBusI.java @@ -0,0 +1,21 @@ +package org.xujin.halo.command; + +import org.xujin.halo.dto.Command; +import org.xujin.halo.dto.Response; + +/** + * + * CommandBus + * + * @author xujin + */ +public interface CommandBusI { + + /** + * Send command to CommandBus, then the command will be executed by CommandExecutor + * + * @param Command or Query + * @return Response + */ + public Response send(Command cmd); +} diff --git a/halo-core/src/main/java/org/xujin/halo/command/CommandExecutorI.java b/halo-core/src/main/java/org/xujin/halo/command/CommandExecutorI.java new file mode 100644 index 0000000..e804208 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/command/CommandExecutorI.java @@ -0,0 +1,15 @@ +package org.xujin.halo.command; + +import org.xujin.halo.dto.Command; +import org.xujin.halo.dto.Response; + +/** + * + * CommandExecutorI + * + * @author xujin + */ +public interface CommandExecutorI { + + public R execute(C cmd); +} diff --git a/halo-core/src/main/java/org/xujin/halo/command/CommandHub.java b/halo-core/src/main/java/org/xujin/halo/command/CommandHub.java new file mode 100644 index 0000000..e9feca9 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/command/CommandHub.java @@ -0,0 +1,53 @@ +package org.xujin.halo.command; + +import org.xujin.halo.exception.InfraException; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.ListMultimap; +import lombok.Getter; +import lombok.Setter; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Command Hub holds all the important information about Command + * + * @author xujin + */ +@SuppressWarnings("rawtypes") +@Component +public class CommandHub{ + + @Getter + @Setter + private ListMultimap preInterceptors = LinkedListMultimap.create(); + @Getter + @Setter + private ListMultimap postInterceptors = LinkedListMultimap.create(); + + @Getter + @Setter + //全局通用的PreInterceptors + private List globalPreInterceptors = new ArrayList<>(); + @Getter + @Setter + //全局通用的PostInterceptors + private List globalPostInterceptors = new ArrayList<>(); + @Getter + @Setter + private Map commandRepository = new HashMap<>(); + + @Getter + private Map responseRepository = new HashMap<>(); + + public CommandInvocation getCommandInvocation(Class cmdClass) { + CommandInvocation commandInvocation = commandRepository.get(cmdClass); + if (commandRepository.get(cmdClass) == null){ + throw new InfraException(cmdClass + " is not registered in CommandHub, please register first"); + } + return commandInvocation; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/command/CommandInterceptorI.java b/halo-core/src/main/java/org/xujin/halo/command/CommandInterceptorI.java new file mode 100644 index 0000000..6700ea5 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/command/CommandInterceptorI.java @@ -0,0 +1,26 @@ +package org.xujin.halo.command; + +import org.xujin.halo.dto.Command; +import org.xujin.halo.dto.Response; + +/** + * Interceptor will do AOP processing before or after Command Execution + * + * @author xujin + */ +public interface CommandInterceptorI { + + /** + * Pre-processing before command execution + * @param command + */ + default public void preIntercept(Command command){}; + + /** + * Post-processing after command execution + * @param command + * @param response, Note that response could be null, check it before use + */ + default public void postIntercept(Command command, Response response){}; + +} diff --git a/halo-core/src/main/java/org/xujin/halo/command/CommandInvocation.java b/halo-core/src/main/java/org/xujin/halo/command/CommandInvocation.java new file mode 100644 index 0000000..f728c44 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/command/CommandInvocation.java @@ -0,0 +1,55 @@ +package org.xujin.halo.command; + +import org.xujin.halo.dto.Command; +import org.xujin.halo.dto.Response; +import com.google.common.collect.FluentIterable; +import lombok.Setter; + +import java.util.List; + +public class CommandInvocation { + + @Setter + private CommandExecutorI commandExecutor; + @Setter + private Iterable preInterceptors; + @Setter + private Iterable postInterceptors; + + public CommandInvocation() { + + } + + public CommandInvocation(CommandExecutorI commandExecutor, List preInterceptors, + List postInterceptors){ + this.commandExecutor = commandExecutor; + this.preInterceptors = preInterceptors; + this.postInterceptors = postInterceptors; + } + + public R invoke(C command) { + R response = null; + try { + preIntercept(command); + response = commandExecutor.execute(command); + response.setSuccess(true); + } + finally { + //make sure post interceptors performs even though exception happens + postIntercept(command, response); + } + return response; + } + + private void postIntercept(C command, R response) { + for (CommandInterceptorI postInterceptor : FluentIterable.from(postInterceptors).toSet()) { + postInterceptor.postIntercept(command, response); + } + } + + private void preIntercept(C command) { + for (CommandInterceptorI preInterceptor : FluentIterable.from(preInterceptors).toSet()) { + preInterceptor.preIntercept(command); + } + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/command/PostInterceptor.java b/halo-core/src/main/java/org/xujin/halo/command/PostInterceptor.java new file mode 100644 index 0000000..789847e --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/command/PostInterceptor.java @@ -0,0 +1,15 @@ +package org.xujin.halo.command; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +@Inherited +@Component +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PostInterceptor { + + Class[] commands() default {}; + +} diff --git a/halo-core/src/main/java/org/xujin/halo/command/PreInterceptor.java b/halo-core/src/main/java/org/xujin/halo/command/PreInterceptor.java new file mode 100644 index 0000000..f1cf85f --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/command/PreInterceptor.java @@ -0,0 +1,15 @@ +package org.xujin.halo.command; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +@Inherited +@Component +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PreInterceptor { + + Class[] commands() default {}; + +} diff --git a/halo-core/src/main/java/org/xujin/halo/command/QueryExecutorI.java b/halo-core/src/main/java/org/xujin/halo/command/QueryExecutorI.java new file mode 100644 index 0000000..66a37fd --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/command/QueryExecutorI.java @@ -0,0 +1,8 @@ +package org.xujin.halo.command; + +import org.xujin.halo.dto.Command; +import org.xujin.halo.dto.Response; + +public interface QueryExecutorI extends CommandExecutorI{ + +} diff --git a/halo-core/src/main/java/org/xujin/halo/common/ApplicationContextHelper.java b/halo-core/src/main/java/org/xujin/halo/common/ApplicationContextHelper.java new file mode 100644 index 0000000..410d179 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/common/ApplicationContextHelper.java @@ -0,0 +1,27 @@ +package org.xujin.halo.common; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * ApplicationContextHelper + * + * @author xujin + * @date 2018-01-07 12:30 PM + */ +@Component +public class ApplicationContextHelper implements ApplicationContextAware{ + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + ApplicationContextHelper.applicationContext = applicationContext; + } + + public static Object getBean(Class claz){ + return ApplicationContextHelper.applicationContext.getBean(claz); + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/common/CoreConstant.java b/halo-core/src/main/java/org/xujin/halo/common/CoreConstant.java new file mode 100644 index 0000000..1fd2d1b --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/common/CoreConstant.java @@ -0,0 +1,22 @@ + +package org.xujin.halo.common; + +/** + * CoreConstant + * @author xujin + */ +public class CoreConstant { + + public final static String DEFAULT_BIZ_CODE = "$defaultBizCode$"; + public final static String DEFAULT_TENANT_ID = "$defaultTenantId$"; + + public final static String EXTENSION_EXTPT_NAMING = "ExtPt"; + public final static String VALIDATOR_NAMING = "Validator"; + public final static String RULE_NAMING = "Rule"; + + public final static String EXTENSIONPOINT_CLASS = "ExtensionPointI"; + public final static String VALIDATORI_CLASS = "ValidatorI"; + public final static String RULEI_CLASS = "RuleI"; + + public final static String EXE_METHOD = "execute"; +} diff --git a/halo-core/src/main/java/org/xujin/halo/common/DefaultBizCode.java b/halo-core/src/main/java/org/xujin/halo/common/DefaultBizCode.java new file mode 100644 index 0000000..08d895f --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/common/DefaultBizCode.java @@ -0,0 +1,13 @@ + +package org.xujin.halo.common; + +/** + * For App BizCode, please define it in your own App, this is for Framework only + * @author xujin + */ +public class DefaultBizCode { + + public final static String DEFAULT_BIZ_CODE = "$defaultBizCode$"; + + +} diff --git a/halo-core/src/main/java/org/xujin/halo/common/LocalAddress.java b/halo-core/src/main/java/org/xujin/halo/common/LocalAddress.java new file mode 100644 index 0000000..11ffb13 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/common/LocalAddress.java @@ -0,0 +1,21 @@ +package org.xujin.halo.common; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 获取本机地址 + * @author xujin + * @date 2018/02/07 + */ +public class LocalAddress { + public static String IP = ""; + + static { + try { + IP = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/context/PvgContext.java b/halo-core/src/main/java/org/xujin/halo/context/PvgContext.java new file mode 100644 index 0000000..c015509 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/context/PvgContext.java @@ -0,0 +1,65 @@ +package org.xujin.halo.context; + +import org.xujin.halo.exception.Preconditions; + +/** + * Pvg Context + *

+ * Created by xujin on 2018/4/9. + */ +public class PvgContext { + + private static ThreadLocal pvgContext = new ThreadLocal<>(); + + private static class Pvg { + private String crmUserId; + private String roleName; + private String orgId; + private String corpId; + + public Pvg(String crmUserId, String roleName, String orgId, String corpId) { + this.crmUserId = crmUserId; + this.roleName = roleName; + this.orgId = orgId; + this.corpId = corpId; + } + } + + public static void set(String crmUserId, String roleName, String orgId, String corpId) { + Pvg pvg = new Pvg(crmUserId, roleName, orgId, corpId); + pvgContext.set(pvg); + } + + public static void remove() { + pvgContext.remove(); + } + + public static boolean exist() { + return null != pvgContext.get(); + } + + public static String getCrmUserId() { + Preconditions.checkArgument(null != pvgContext.get() && null != pvgContext.get().crmUserId, + "No User in Context"); + return pvgContext.get().crmUserId; + } + + public static String getRoleName() { + Preconditions.checkArgument(null != pvgContext.get() && null != pvgContext.get().roleName, + "No roleName in Context"); + return pvgContext.get().roleName; + } + + public static String getOrgId() { + Preconditions.checkArgument(null != pvgContext.get() && null != pvgContext.get().orgId, + "No orgId in Context"); + return pvgContext.get().orgId; + } + + public static String getCorpId() { + Preconditions.checkArgument(null != pvgContext.get() && null != pvgContext.get().corpId, + "No corpId in Context"); + return pvgContext.get().corpId; + } + +} diff --git a/halo-core/src/main/java/org/xujin/halo/context/TenantContext.java b/halo-core/src/main/java/org/xujin/halo/context/TenantContext.java new file mode 100644 index 0000000..e74536e --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/context/TenantContext.java @@ -0,0 +1,49 @@ +package org.xujin.halo.context; + +import org.xujin.halo.exception.BizException; + +/** + * 租户的上下文 + */ +public class TenantContext { + + private static ThreadLocal tenantContext = new ThreadLocal<>(); + + private static class Tenant{ + String tenantId; + String bizCode; + private Tenant(String tenantId, String bizCode) { + this.tenantId = tenantId; + this.bizCode = bizCode; + } + } + + public static boolean exist(){ + return null != tenantContext.get(); + } + + public static String getTenantId() { + if (tenantContext.get() == null || tenantContext.get().tenantId == null) { + throw new BizException("No tenantId in Context"); + } + return tenantContext.get().tenantId; + } + + public static String getBizCode() { + if (tenantContext.get() == null || tenantContext.get().bizCode == null) { + throw new BizException("No bizCode in Context"); + } + return tenantContext.get().bizCode; + } + + public static void set(String tenantId, String bizCode) { + Tenant tenant = new Tenant(tenantId, bizCode); + tenantContext.set(tenant); + } + + public static void remove() { + tenantContext.remove(); + } + +} + diff --git a/halo-core/src/main/java/org/xujin/halo/convertor/ConvertorI.java b/halo-core/src/main/java/org/xujin/halo/convertor/ConvertorI.java new file mode 100644 index 0000000..2ecf945 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/convertor/ConvertorI.java @@ -0,0 +1,25 @@ +package org.xujin.halo.convertor; + +import org.xujin.halo.domain.Entity; +import org.xujin.halo.dto.DTO; +import org.xujin.halo.repository.DataObject; + +/** + * Convertor are used to convert Objects among Client Object, Domain Object and Data Object. + * + * @author xujin + */ +public interface ConvertorI { + + default public T entityToClient(Object... entityObject){return null;} + + default public T dataToClient(Object... dataObject){return null;} + + default public T clientToEntity(Object... clientObject){return null;} + + default public T dataToEntity(Object... dataObject){return null;} + + default public T entityToData(Object... entityObject){return null;} + +} + diff --git a/halo-core/src/main/java/org/xujin/halo/event/EventBus.java b/halo-core/src/main/java/org/xujin/halo/event/EventBus.java new file mode 100644 index 0000000..e915a5f --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/event/EventBus.java @@ -0,0 +1,103 @@ +package org.xujin.halo.event; + +import org.xujin.halo.dto.Response; +import org.xujin.halo.dto.event.Event; +import org.xujin.halo.exception.BasicErrorCode; +import org.xujin.halo.exception.HaloException; +import org.xujin.halo.exception.ErrorCodeI; +import org.xujin.halo.exception.InfraException; +import org.xujin.halo.logger.Logger; +import org.xujin.halo.logger.LoggerFactory; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.*; +import java.util.stream.Collectors; + +/** + * Event Bus + * + * @author xujin + * @date 2018/3/20 + */ +@Component +public class EventBus implements EventBusI{ + Logger logger = LoggerFactory.getLogger(EventBus.class); + + /** + * 默认线程池 + * 如果处理器无定制线程池,则使用此默认 + */ + ExecutorService defaultExecutor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors() + 1, + Runtime.getRuntime().availableProcessors() + 1, + 0L,TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(1000),new ThreadFactoryBuilder().setNameFormat("event-bus-pool-%d").build()); + + @Autowired + private EventHub eventHub; + + @SuppressWarnings("unchecked") + @Override + public Response fire(Event event) { + Response response = null; + EventHandlerI eventHandlerI = null; + try { + eventHandlerI = eventHub.getEventHandler(event.getClass()).get(0); + response = eventHandlerI.execute(event); + }catch (Exception exception) { + response = handleException(eventHandlerI, response, exception); + } + return response; + } + + @Override + public void fireAll(Event event){ + eventHub.getEventHandler(event.getClass()).parallelStream().map(p->{ + Response response = null; + try { + response = p.execute(event); + }catch (Exception exception) { + response = handleException(p, response, exception); + } + return response; + }).collect(Collectors.toList()); + } + + @Override + public void asyncFire(Event event){ + eventHub.getEventHandler(event.getClass()).parallelStream().map(p->{ + Response response = null; + try { + if(null != p.getExecutor()){ + p.getExecutor().submit(()->p.execute(event)); + }else{ + defaultExecutor.submit(()->p.execute(event)); + } + }catch (Exception exception) { + response = handleException(p, response, exception); + } + return response; + }).collect(Collectors.toList()); + } + + private Response handleException(EventHandlerI handler, Response response, Exception exception) { + Class responseClz = eventHub.getResponseRepository().get(handler.getClass()); + try { + response = (Response) responseClz.newInstance(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + throw new InfraException(e.getMessage()); + } + if (exception instanceof HaloException) { + ErrorCodeI errCode = ((HaloException) exception).getErrCode(); + response.setErrCode(errCode.getErrCode()); + } + else { + response.setErrCode(BasicErrorCode.SYS_ERROR.getErrCode()); + } + response.setErrMessage(exception.getMessage()); + logger.error(exception.getMessage(), exception); + return response; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/event/EventBusI.java b/halo-core/src/main/java/org/xujin/halo/event/EventBusI.java new file mode 100644 index 0000000..923956e --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/event/EventBusI.java @@ -0,0 +1,36 @@ +package org.xujin.halo.event; + +import org.xujin.halo.dto.event.Event; +import org.xujin.halo.dto.Response; + + +/** + * EventBus interface + * @author xujin + * @date 2018/3/20 + */ +public interface EventBusI { + + /** + * Send event to EventBus + * 发送事件到事件Bus + * @param event + * @return Response + */ + public Response fire(Event event); + + /** + * fire all handlers which registed the event + * 触发已经注册的所有注册事件对应的处理器 + * @param event + * @return Response + */ + public void fireAll(Event event); + + /** + * 异步触发对应注册事件的对应的处理器 + * Async fire all handlers + * @param event + */ + public void asyncFire(Event event); +} diff --git a/halo-core/src/main/java/org/xujin/halo/event/EventHandler.java b/halo-core/src/main/java/org/xujin/halo/event/EventHandler.java new file mode 100644 index 0000000..85564cd --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/event/EventHandler.java @@ -0,0 +1,17 @@ +package org.xujin.halo.event; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * 事件处理器 + * @author xujin + * @date 2018/5/20 + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Component +public @interface EventHandler { +} diff --git a/halo-core/src/main/java/org/xujin/halo/event/EventHandlerI.java b/halo-core/src/main/java/org/xujin/halo/event/EventHandlerI.java new file mode 100644 index 0000000..fc39464 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/event/EventHandlerI.java @@ -0,0 +1,22 @@ +package org.xujin.halo.event; + +import org.xujin.halo.dto.event.Event; +import org.xujin.halo.dto.Response; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; + +/** + * event handler + * 事件处理器的抽象接口 + * @author xujin + * @date 2018/6/20 + */ +public interface EventHandlerI { + + default public ExecutorService getExecutor(){ + return null; + } + + public R execute(E e); +} diff --git a/halo-core/src/main/java/org/xujin/halo/event/EventHub.java b/halo-core/src/main/java/org/xujin/halo/event/EventHub.java new file mode 100644 index 0000000..eb99dfa --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/event/EventHub.java @@ -0,0 +1,54 @@ +package org.xujin.halo.event; + +import org.xujin.halo.dto.event.Event; +import org.xujin.halo.exception.InfraException; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; +import lombok.Getter; +import lombok.Setter; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 事件控制中枢 + * @author xujin + * @date 2018/5/20 + */ +@SuppressWarnings("rawtypes") +@Component +public class EventHub { + @Getter + @Setter + private ListMultimap eventRepository = ArrayListMultimap.create(); + + @Getter + private Map responseRepository = new HashMap<>(); + + public List getEventHandler(Class eventClass) { + List eventHandlerIList = findHandler(eventClass); + if (eventHandlerIList == null || eventHandlerIList.size() == 0) { + throw new InfraException(eventClass + "is not registered in eventHub, please register first"); + } + return eventHandlerIList; + } + + /** + * 注册事件 + * @param eventClz + * @param executor + */ + public void register(Class eventClz, EventHandlerI executor){ + eventRepository.put(eventClz, executor); + } + + private List findHandler(Class eventClass){ + List eventHandlerIList = null; + Class cls = eventClass; + eventHandlerIList = eventRepository.get(cls); + return eventHandlerIList; + } + +} diff --git a/halo-core/src/main/java/org/xujin/halo/exception/BasicErrorCode.java b/halo-core/src/main/java/org/xujin/halo/exception/BasicErrorCode.java new file mode 100644 index 0000000..8004a87 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/exception/BasicErrorCode.java @@ -0,0 +1,42 @@ +package org.xujin.halo.exception; + +/** + * + * 参数错误:1000 - 1999 + * 业务错误:2000 - 2999 + * 基础错误:3000 - 3999 + * 系统错误:4000 - 4999 + * + * 基本的错误定义 + */ +public enum BasicErrorCode implements ErrorCodeI{ + PARAM_ERROR("1000" , "请求参数校验错误" , false), + BIZ_ERROR("2000" , "业务逻辑错误" , false), + INFRA_ERROR("3000" , "基础设施(数据库,缓存,消息等)错误" , true), + SYS_ERROR("4000" , "未知的其它系统错误" , true); + + private String errCode; + private String errDesc; + private boolean retriable; + + private BasicErrorCode(String errCode, String errDesc, boolean retriable){ + this.errCode = errCode; + this.errDesc = errDesc; + this.retriable = retriable; + } + + @Override + public String getErrCode() { + return errCode; + } + + @Override + public String getErrDesc() { + return errDesc; + } + + @Override + public boolean isRetriable() { + return retriable; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/exception/BizException.java b/halo-core/src/main/java/org/xujin/halo/exception/BizException.java new file mode 100644 index 0000000..1179e0c --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/exception/BizException.java @@ -0,0 +1,21 @@ +package org.xujin.halo.exception; + +public class BizException extends HaloException { + + private static final long serialVersionUID = 1L; + + public BizException(String errMessage){ + super(errMessage); + this.setErrCode(BasicErrorCode.BIZ_ERROR); + } + + public BizException(ErrorCodeI errCode, String errMessage){ + super(errMessage); + this.setErrCode(errCode); + } + + public BizException(String errMessage, Throwable e) { + super(errMessage, e); + this.setErrCode(BasicErrorCode.BIZ_ERROR); + } +} \ No newline at end of file diff --git a/halo-core/src/main/java/org/xujin/halo/exception/ErrorCodeI.java b/halo-core/src/main/java/org/xujin/halo/exception/ErrorCodeI.java new file mode 100644 index 0000000..a3550a8 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/exception/ErrorCodeI.java @@ -0,0 +1,15 @@ +package org.xujin.halo.exception; + +/** + * Extends your error codes in your App by implements this Interface. + * + * Created by xujin on 2018/4/18. + */ +public interface ErrorCodeI { + + public String getErrCode(); + + public String getErrDesc(); + + public boolean isRetriable(); +} diff --git a/halo-core/src/main/java/org/xujin/halo/exception/HaloException.java b/halo-core/src/main/java/org/xujin/halo/exception/HaloException.java new file mode 100644 index 0000000..51ca528 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/exception/HaloException.java @@ -0,0 +1,31 @@ +package org.xujin.halo.exception; + +/** + * + * Halo框架抽象异常 + * + * @author + */ +public abstract class HaloException extends RuntimeException{ + + private static final long serialVersionUID = 1L; + + private ErrorCodeI errCode; + + public HaloException(String errMessage){ + super(errMessage); + } + + public HaloException(String errMessage, Throwable e) { + super(errMessage, e); + } + + public ErrorCodeI getErrCode() { + return errCode; + } + + public void setErrCode(ErrorCodeI errCode) { + this.errCode = errCode; + } + +} diff --git a/halo-core/src/main/java/org/xujin/halo/exception/InfraException.java b/halo-core/src/main/java/org/xujin/halo/exception/InfraException.java new file mode 100644 index 0000000..2b73a43 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/exception/InfraException.java @@ -0,0 +1,22 @@ +package org.xujin.halo.exception; + +/** + * + * Infrastructure Exception + * 基础设施层异常 + * @author xujin + */ +public class InfraException extends HaloException { + + private static final long serialVersionUID = 1L; + + public InfraException(String errMessage){ + super(errMessage); + this.setErrCode(BasicErrorCode.INFRA_ERROR); + } + + public InfraException(String errMessage, Throwable e) { + super(errMessage, e); + this.setErrCode(BasicErrorCode.INFRA_ERROR); + } +} \ No newline at end of file diff --git a/halo-core/src/main/java/org/xujin/halo/exception/ParamException.java b/halo-core/src/main/java/org/xujin/halo/exception/ParamException.java new file mode 100644 index 0000000..ec87819 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/exception/ParamException.java @@ -0,0 +1,24 @@ +package org.xujin.halo.exception; + +/** + * 参数异常 + */ +public class ParamException extends HaloException { + + private static final long serialVersionUID = 1L; + + public ParamException(String errMessage){ + super(errMessage); + this.setErrCode(BasicErrorCode.PARAM_ERROR); + } + + public ParamException(ErrorCodeI errCode, String errMessage){ + super(errMessage); + this.setErrCode(errCode); + } + + public ParamException(String errMessage, Throwable e) { + super(errMessage, e); + this.setErrCode(BasicErrorCode.PARAM_ERROR); + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/exception/Preconditions.java b/halo-core/src/main/java/org/xujin/halo/exception/Preconditions.java new file mode 100644 index 0000000..1e8e9f9 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/exception/Preconditions.java @@ -0,0 +1,32 @@ +package org.xujin.halo.exception; + +/** + * Preconditions 先决条件 + * @author xujin + */ +public class Preconditions { + + public static void checkArgument(boolean expression) { + if (!expression) { + throw new ParamException(""); + } + } + + public static void checkArgument(boolean expression, Object errorMessage) { + if (!expression) { + throw new ParamException(String.valueOf(errorMessage)); + } + } + + public static void checkState(boolean expression) { + if (!expression) { + throw new BizException(""); + } + } + + public static void checkState(boolean expression, Object errorMessage) { + if (!expression) { + throw new BizException(String.valueOf(errorMessage)); + } + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/exception/SystemException.java b/halo-core/src/main/java/org/xujin/halo/exception/SystemException.java new file mode 100644 index 0000000..832d6e6 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/exception/SystemException.java @@ -0,0 +1,25 @@ +package org.xujin.halo.exception; + +/** + * 系统异常 + * + * @author xujin + */ +public class SystemException extends HaloException { + + private static final long serialVersionUID = 4355163994767354840L; + + public SystemException(String errMessage) { + super(errMessage); + } + + public SystemException(ErrorCodeI errCode, String errMessage) { + super(errMessage); + this.setErrCode(errCode); + } + + public SystemException(String errMessage, ErrorCodeI errorCode, Throwable e) { + super(errMessage, e); + this.setErrCode(errorCode); + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/extension/Extension.java b/halo-core/src/main/java/org/xujin/halo/extension/Extension.java new file mode 100644 index 0000000..12d4cf9 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/extension/Extension.java @@ -0,0 +1,30 @@ +package org.xujin.halo.extension; + +import org.springframework.core.annotation.AliasFor; +import org.xujin.halo.common.CoreConstant; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * Extension + * @author xujin + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Component +public @interface Extension { + + /** + * 一级业务code + * @return + */ + String bizCode() default CoreConstant.DEFAULT_BIZ_CODE; + + /** + * 二级业务code + * @return + */ + String tenantId() default CoreConstant.DEFAULT_TENANT_ID; +} diff --git a/halo-core/src/main/java/org/xujin/halo/extension/ExtensionCoordinate.java b/halo-core/src/main/java/org/xujin/halo/extension/ExtensionCoordinate.java new file mode 100644 index 0000000..1e1b61c --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/extension/ExtensionCoordinate.java @@ -0,0 +1,61 @@ +package org.xujin.halo.extension; + +import lombok.Data; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * Extension Coordinate(扩展点坐标) used to uniquely position a Extension + * @author xujin + */ +@Data +public class ExtensionCoordinate { + + private String extensionPoint; + private String bizCode; + private String tenantId; + + /** + * @param extensionPoint + * @param bizCode + * @param tenantId + */ + public ExtensionCoordinate(String extensionPoint, String bizCode, String tenantId){ + super(); + this.extensionPoint = extensionPoint; + this.bizCode = bizCode; + this.tenantId = tenantId; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((bizCode == null) ? 0 : bizCode.hashCode()); + result = prime * result + ((extensionPoint == null) ? 0 : extensionPoint.hashCode()); + result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode()); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + ExtensionCoordinate other = (ExtensionCoordinate) obj; + if (bizCode == null) { + if (other.bizCode != null) return false; + } else if (!bizCode.equals(other.bizCode)) return false; + if (extensionPoint == null) { + if (other.extensionPoint != null) return false; + } else if (!extensionPoint.equals(other.extensionPoint)) return false; + if (tenantId == null) { + if (other.tenantId != null) return false; + } else if (!tenantId.equals(other.tenantId)) return false; + return true; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/extension/ExtensionExecutor.java b/halo-core/src/main/java/org/xujin/halo/extension/ExtensionExecutor.java new file mode 100644 index 0000000..9e4f1b8 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/extension/ExtensionExecutor.java @@ -0,0 +1,65 @@ +package org.xujin.halo.extension; + +import org.xujin.halo.boot.ComponentExecutor; +import org.xujin.halo.common.CoreConstant; +import org.xujin.halo.common.DefaultBizCode; +import org.xujin.halo.context.TenantContext; +import org.xujin.halo.exception.InfraException; +import org.xujin.halo.logger.Logger; +import org.xujin.halo.logger.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 扩展点执行器-ExtensionExecutor + * @author xujin + */ +@Component +public class ExtensionExecutor extends ComponentExecutor{ + + private Logger logger = LoggerFactory.getLogger(ExtensionExecutor.class); + + @Autowired + private ExtensionRepository extensionRepository; + + @Override + protected C locateComponent(Class targetClz) { + C extension = locateExtension(targetClz); + logger.debug("[Located Extension]: "+extension.getClass().getSimpleName()); + return locateExtension(targetClz); + } + + /** + * @param targetClz + */ + @SuppressWarnings("unchecked") + protected Ext locateExtension(Class targetClz) { + String bizCode = TenantContext.getBizCode(); + String tenantId = TenantContext.getTenantId(); + ExtensionCoordinate extensionCoordinate = new ExtensionCoordinate(targetClz.getSimpleName(), bizCode, tenantId); + /** + * 1.First search key is: extensionPoint + bizCode + tenantId + */ + Ext extension = (Ext)extensionRepository.getExtensionRepo().get(extensionCoordinate); + if (extension != null) { + return extension; + } + /** + * 2.Second search key is: extensionPoint + bizCode + */ + extensionCoordinate.setTenantId(CoreConstant.DEFAULT_TENANT_ID); + extension = (Ext)extensionRepository.getExtensionRepo().get(extensionCoordinate); + if (extension != null) { + return extension; + } + /** + * 3.Third search key is: extensionPoint + */ + extensionCoordinate.setBizCode(DefaultBizCode.DEFAULT_BIZ_CODE); + extension = (Ext)extensionRepository.getExtensionRepo().get(extensionCoordinate); + if (extension != null) { + return extension; + } + throw new InfraException("Can not find extension for ExtensionPoint: "+targetClz); + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/extension/ExtensionPointI.java b/halo-core/src/main/java/org/xujin/halo/extension/ExtensionPointI.java new file mode 100644 index 0000000..3f4bf6a --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/extension/ExtensionPointI.java @@ -0,0 +1,10 @@ +package org.xujin.halo.extension; + +/** + * ExtensionPointI is the parent interface of all ExtensionPoints + * 扩展点表示一块逻辑在不同的业务有不同的实现,使用扩展点做接口申明,然后用Extension(扩展)去实现扩展点。 + * @author xujin + */ +public interface ExtensionPointI { + +} diff --git a/halo-core/src/main/java/org/xujin/halo/extension/ExtensionRepository.java b/halo-core/src/main/java/org/xujin/halo/extension/ExtensionRepository.java new file mode 100644 index 0000000..216fe27 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/extension/ExtensionRepository.java @@ -0,0 +1,20 @@ +package org.xujin.halo.extension; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.stereotype.Component; + +import lombok.Getter; + +/** + * ExtensionRepository + * @author xujin + */ +@Component +public class ExtensionRepository { + + @Getter + private Map extensionRepo = new HashMap<>(); + +} diff --git a/halo-core/src/main/java/org/xujin/halo/extensionpoint/MqEventConsumerExtPt.java b/halo-core/src/main/java/org/xujin/halo/extensionpoint/MqEventConsumerExtPt.java new file mode 100644 index 0000000..7d4b8cd --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/extensionpoint/MqEventConsumerExtPt.java @@ -0,0 +1,13 @@ +package org.xujin.halo.extensionpoint; + +import org.xujin.halo.dto.Response; + +/** + * 消息消费扩展点 + * @author xujin + * + */ +public interface MqEventConsumerExtPt { + + Response receive(String msg); +} diff --git a/halo-core/src/main/java/org/xujin/halo/logger/Logger.java b/halo-core/src/main/java/org/xujin/halo/logger/Logger.java new file mode 100644 index 0000000..448e493 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/logger/Logger.java @@ -0,0 +1,84 @@ +package org.xujin.halo.logger; + +/** + * This is a typical DIP, by depending on ourselves logger, we can change the underline logger vendor easily + * + * @author xujin 2017年10月23日 下午11:58:54 + */ +public interface Logger { + + /** + * Log a message at the DEBUG level. + * + * @param msg the message string to be logged + */ + public void debug(String msg); + + /** + * Log a message at the DEBUG level. support format. + * + * @param msg + * @param args + */ + default public void debug(String msg, Object... args){ + debug(String.format(msg, args)); + } + + /** + * Log a message at the INFO level. + * + * @param msg the message string to be logged + */ + public void info(String msg); + + /** + * Log a message at the INFO level. support format. + * + * @param msg + * @param args + */ + default public void info(String msg, Object... args){ + info(String.format(msg, args)); + } + + /** + * Log a message at the WARN level. + * + * @param msg the message string to be logged + */ + public void warn(String msg); + + /** + * Log a message at the WARN level. support format. + * + * @param msg + * @param args + */ + default public void warn(String msg, Object... args){ + warn(String.format(msg, args)); + } + /** + * Log a message at the ERROR level. + * + * @param msg the message string to be logged + */ + public void error(String msg); + + /** + * Log a message at the ERROR level. support format. + * @param msg + * @param args + */ + default public void error(String msg, Object... args){ + error(String.format(msg, args)); + } + + /** + * Log an exception (throwable) at the ERROR level with an + * accompanying message. + * + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + public void error(String msg, Throwable t); +} diff --git a/halo-core/src/main/java/org/xujin/halo/logger/LoggerFactory.java b/halo-core/src/main/java/org/xujin/halo/logger/LoggerFactory.java new file mode 100644 index 0000000..e769c85 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/logger/LoggerFactory.java @@ -0,0 +1,33 @@ +package org.xujin.halo.logger; + +public class LoggerFactory { + + private static boolean useSysLogger = true; + + public static Logger getLogger(Class clazz) { + if(useSysLogger) { + return SysLogger.singleton; + } + org.slf4j.Logger slfjLogger = org.slf4j.LoggerFactory.getLogger(clazz); + return new SLFJLogger(slfjLogger); + } + + public static Logger getLogger(String loggerName) { + if(useSysLogger) { + return SysLogger.singleton; + } + org.slf4j.Logger slfjLogger = org.slf4j.LoggerFactory.getLogger(loggerName); + return new SLFJLogger(slfjLogger); + } + + /** + * This is just for test purpose, don't use it on product! + */ + public static void activateSysLogger() { + useSysLogger = true; + } + + public static void deactivateSysLogger() { + useSysLogger = false; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/logger/SLFJLogger.java b/halo-core/src/main/java/org/xujin/halo/logger/SLFJLogger.java new file mode 100644 index 0000000..c24eb87 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/logger/SLFJLogger.java @@ -0,0 +1,39 @@ +package org.xujin.halo.logger; + +public class SLFJLogger implements Logger { + + private org.slf4j.Logger slfjLogger; + + public SLFJLogger(org.slf4j.Logger slfjLogger) { + this.slfjLogger = slfjLogger; + } + + @Override + public void debug(String msg) { + slfjLogger.debug(msg); + } + + @Override + public void info(String msg) { + slfjLogger.info(msg); + + } + + @Override + public void warn(String msg) { + slfjLogger.warn(msg); + + } + + @Override + public void error(String msg) { + slfjLogger.error(msg); + + } + + @Override + public void error(String msg, Throwable t) { + slfjLogger.error(msg, t); + } + +} diff --git a/halo-core/src/main/java/org/xujin/halo/logger/SysLogger.java b/halo-core/src/main/java/org/xujin/halo/logger/SysLogger.java new file mode 100644 index 0000000..8302b9d --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/logger/SysLogger.java @@ -0,0 +1,33 @@ +package org.xujin.halo.logger; + +public class SysLogger implements Logger{ + + public static SysLogger singleton = new SysLogger(); + + @Override + public void debug(String msg) { + System.out.println("DEBUG: "+msg); + } + + @Override + public void info(String msg) { + System.out.println("INFO: "+msg); + } + + @Override + public void warn(String msg) { + System.out.println("WARN: "+msg); + } + + @Override + public void error(String msg) { + System.err.println("ERROR: "+msg); + } + + @Override + public void error(String msg, Throwable t) { + System.err.println("ERROR: "+msg); + t.printStackTrace(); + } + +} diff --git a/halo-core/src/main/java/org/xujin/halo/pattern/filter/Filter.java b/halo-core/src/main/java/org/xujin/halo/pattern/filter/Filter.java new file mode 100644 index 0000000..daa2081 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/pattern/filter/Filter.java @@ -0,0 +1,10 @@ +package org.xujin.halo.pattern.filter; + +/** + * + */ +public interface Filter { + + void doFilter(Object context, FilterInvoker nextFilter); + +} \ No newline at end of file diff --git a/halo-core/src/main/java/org/xujin/halo/pattern/filter/FilterChain.java b/halo-core/src/main/java/org/xujin/halo/pattern/filter/FilterChain.java new file mode 100644 index 0000000..56a8ceb --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/pattern/filter/FilterChain.java @@ -0,0 +1,19 @@ +package org.xujin.halo.pattern.filter; + +/** + * 拦截器链 + * @author xujin + * @date 2018/04/17 + */ +public class FilterChain{ + + private FilterInvoker header; + + public void doFilter(Object context){ + header.invoke(context); + } + + public void setHeader(FilterInvoker header) { + this.header = header; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/pattern/filter/FilterChainFactory.java b/halo-core/src/main/java/org/xujin/halo/pattern/filter/FilterChainFactory.java new file mode 100644 index 0000000..7946d50 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/pattern/filter/FilterChainFactory.java @@ -0,0 +1,30 @@ +package org.xujin.halo.pattern.filter; + +import org.xujin.halo.common.ApplicationContextHelper; + +/** + * 责任链模式工厂 + * @author xujin + * @date 2018/04/17 + */ +public class FilterChainFactory { + + public static FilterChain buildFilterChain(Class... filterClsList) { + FilterInvoker last = new FilterInvoker(){}; + FilterChain filterChain = new FilterChain(); + for(int i = filterClsList.length - 1; i >= 0; i--){ + FilterInvoker next = last; + Filter filter = (Filter)ApplicationContextHelper.getBean(filterClsList[i]); + last = new FilterInvoker(){ + @Override + public void invoke(Object context) { + filter.doFilter(context, next); + } + }; + } + filterChain.setHeader(last); + return filterChain; + } + + +} diff --git a/halo-core/src/main/java/org/xujin/halo/pattern/filter/FilterInvoker.java b/halo-core/src/main/java/org/xujin/halo/pattern/filter/FilterInvoker.java new file mode 100644 index 0000000..c95d0a8 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/pattern/filter/FilterInvoker.java @@ -0,0 +1,10 @@ +package org.xujin.halo.pattern.filter; + +/** + * @author xujin + * @date 2018/04/17 + */ +public interface FilterInvoker { + + default public void invoke(Object context){}; +} diff --git a/halo-core/src/main/java/org/xujin/halo/repository/DataObject.java b/halo-core/src/main/java/org/xujin/halo/repository/DataObject.java new file mode 100644 index 0000000..c9a923c --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/repository/DataObject.java @@ -0,0 +1,11 @@ +package org.xujin.halo.repository; + +import java.io.Serializable; + +/** + * This is the parent of all Data Objects. + * Data object only has fields and according getters and setters, you can also call it anemic objects + * @author xujin 2017年10月27日 上午10:21:01 + */ +public interface DataObject extends Serializable { +} diff --git a/halo-core/src/main/java/org/xujin/halo/repository/DataTunnel.java b/halo-core/src/main/java/org/xujin/halo/repository/DataTunnel.java new file mode 100644 index 0000000..06263a5 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/repository/DataTunnel.java @@ -0,0 +1,23 @@ +package org.xujin.halo.repository; + +import java.util.List; + +/** + * Data Tunnel is the real Data CRUD Operator may interact with DB, service, cache etc... + * + * @author xujin 2017年10月27日 上午10:34:17 + */ +public interface DataTunnel { + + public T create(T dataObject); + + default public void delete(T dataObject) {}; + + default public void update(T dataObject){}; + + default public List list(T entity){return null;}; + + public T get(String id); + + public List getByEntity(T entity); +} diff --git a/halo-core/src/main/java/org/xujin/halo/repository/RepositoryI.java b/halo-core/src/main/java/org/xujin/halo/repository/RepositoryI.java new file mode 100644 index 0000000..692eb80 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/repository/RepositoryI.java @@ -0,0 +1,9 @@ +package org.xujin.halo.repository; + +/** + * This is Repository pattern which decouples the data access from concrete data tunnels. + * + * @author xujin 2017年10月27日 上午11:07:26 + */ +public interface RepositoryI { +} diff --git a/halo-core/src/main/java/org/xujin/halo/rule/PlainRuleRepository.java b/halo-core/src/main/java/org/xujin/halo/rule/PlainRuleRepository.java new file mode 100644 index 0000000..cc66685 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/rule/PlainRuleRepository.java @@ -0,0 +1,18 @@ +package org.xujin.halo.rule; + +import lombok.Getter; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * Plain Rule is Rule Component without ExtensionPoint + * @author xujin + * @date 2018/5/21 + */ +@Component +public class PlainRuleRepository { + @Getter + private Map, RuleI> plainRules = new HashMap<>(); +} diff --git a/halo-core/src/main/java/org/xujin/halo/rule/RuleExecutor.java b/halo-core/src/main/java/org/xujin/halo/rule/RuleExecutor.java new file mode 100644 index 0000000..2c860a0 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/rule/RuleExecutor.java @@ -0,0 +1,30 @@ +package org.xujin.halo.rule; + +import org.xujin.halo.extension.ExtensionExecutor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * RuleExecutor + * + * Note that Rule is extensible as long as @Extension is added + * + * @author xujin 2017-11-04 + */ +@Component +public class RuleExecutor extends ExtensionExecutor { + + @Autowired + private PlainRuleRepository plainRuleRepository; + + @Override + protected C locateComponent(Class targetClz) { + C rule = (C) plainRuleRepository.getPlainRules().get(targetClz); + return null != rule ? rule : super.locateComponent(targetClz); + } + + public void validate(Class targetClz, Object... candidate) { + RuleI rule = this.locateComponent(targetClz); + rule.validate(candidate); + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/rule/RuleI.java b/halo-core/src/main/java/org/xujin/halo/rule/RuleI.java new file mode 100644 index 0000000..54aea7f --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/rule/RuleI.java @@ -0,0 +1,24 @@ +package org.xujin.halo.rule; + +/** + * 业务规则接口,适合领域层的业务规则检查 + * @author xueliang.sxl + * + */ +public interface RuleI { + + /** + * 默认的规则检查接口,子接口可自定义 + * @param roleContext + * @return + */ + default boolean check(Object roleContext){ + return true; + } + + /** + * This is specific for Validation cases where no Return is needed + * @param candidate + */ + default void validate(Object... candidate){} +} diff --git a/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/AbstractRule.java b/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/AbstractRule.java new file mode 100644 index 0000000..724f515 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/AbstractRule.java @@ -0,0 +1,24 @@ +package org.xujin.halo.rule.ruleengine; + +import org.xujin.halo.rule.RuleI; + +/** + * 业务规则的抽象实现,可用于组合规则 + * @author xueliang.sxl + * + */ +public abstract class AbstractRule implements RuleI { + + public RuleI and(RuleI other) { + return new AndRule(this, other); + } + + public RuleI or(RuleI other) { + return new OrRule(this, other); + } + + public RuleI not() { + return new NotRule(this); + } + +} diff --git a/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/AndRule.java b/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/AndRule.java new file mode 100644 index 0000000..353d49b --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/AndRule.java @@ -0,0 +1,24 @@ +package org.xujin.halo.rule.ruleengine; + +import org.xujin.halo.rule.RuleI; + +/** + * 组合And的业务规则 + * @author xueliang.sxl + * + */ +public class AndRule extends AbstractRule { + RuleI one; + RuleI other; + + public AndRule(RuleI x, RuleI y){ + one = x; + other = y; + } + + @Override + public boolean check(Object candidate) { + return one.check(candidate) && other.check(candidate); + } + +} diff --git a/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/NotRule.java b/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/NotRule.java new file mode 100644 index 0000000..af23899 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/NotRule.java @@ -0,0 +1,22 @@ +package org.xujin.halo.rule.ruleengine; + +import org.xujin.halo.rule.RuleI; + +/** + * 组合Not的业务规则 + * @author xueliang.sxl + * + */ +public class NotRule extends AbstractRule { + RuleI wrapped; + + public NotRule(RuleI x){ + wrapped = x; + } + + @Override + public boolean check(Object candidate) { + return !wrapped.check(candidate); + } + +} \ No newline at end of file diff --git a/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/OrRule.java b/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/OrRule.java new file mode 100644 index 0000000..a654524 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/rule/ruleengine/OrRule.java @@ -0,0 +1,24 @@ +package org.xujin.halo.rule.ruleengine; + +import org.xujin.halo.rule.RuleI; + +/** + * 组合Or的业务规则 + * @author xueliang.sxl + * + */ +public class OrRule extends AbstractRule { + RuleI one; + RuleI other; + + public OrRule(RuleI x, RuleI y){ + one = x; + other = y; + } + + @Override + public boolean check(Object candidate) { + return one.check(candidate) || other.check(candidate); + } + +} \ No newline at end of file diff --git a/halo-core/src/main/java/org/xujin/halo/validator/HaloMessageInterpolator.java b/halo-core/src/main/java/org/xujin/halo/validator/HaloMessageInterpolator.java new file mode 100644 index 0000000..09b6f26 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/validator/HaloMessageInterpolator.java @@ -0,0 +1,19 @@ +package org.xujin.halo.validator; + +import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator; + +import java.util.Locale; + +/** + * @author xujin + * @date 2018/6/25 + */ +public class HaloMessageInterpolator extends ResourceBundleMessageInterpolator{ + + @Override + public String interpolate(String message, Context context) { + //Use English Locale + String resolvedMessage = super.interpolate(message, context, Locale.ENGLISH); + return resolvedMessage; + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/validator/PlainValidatorRepository.java b/halo-core/src/main/java/org/xujin/halo/validator/PlainValidatorRepository.java new file mode 100644 index 0000000..5837ccd --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/validator/PlainValidatorRepository.java @@ -0,0 +1,18 @@ +package org.xujin.halo.validator; + +import lombok.Getter; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * Plain Validator is Validator Component without ExtensionPoint + * @author xujin + * @date 2018/6/21 + */ +@Component +public class PlainValidatorRepository { + @Getter + private Map, ValidatorI> plainValidators = new HashMap<>(); +} diff --git a/halo-core/src/main/java/org/xujin/halo/validator/ValidatorCompoiste.java b/halo-core/src/main/java/org/xujin/halo/validator/ValidatorCompoiste.java new file mode 100644 index 0000000..8da88f1 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/validator/ValidatorCompoiste.java @@ -0,0 +1,50 @@ +package org.xujin.halo.validator; + +import com.google.common.collect.Lists; +import org.springframework.beans.factory.InitializingBean; + +import java.util.List; + +/** + * 使用组合模式,允许一个Validator可以组合多个其它的Validator完成validation的任务 + * + * @author xujin 2017-11-04 + */ + +public abstract class ValidatorCompoiste implements ValidatorI, InitializingBean { + + private List validators; + + /** + * Composite other validators if necessary + */ + abstract protected void addOtherValidators(); + + /** + * Aside from composited validators, do its own validation here + * @param candidate + */ + abstract protected void doValidate(Object candidate); + + protected void add(ValidatorI validator) { + if (validators == null) { + validators = Lists.newArrayList(); + } + validators.add(validator); + } + + @Override + public void validate(Object candidate) { + if (validators != null) { + for (ValidatorI validator : validators) { + validator.validate(candidate); + } + } + doValidate(candidate); + } + + @Override + public void afterPropertiesSet() throws Exception { + addOtherValidators(); + } +} diff --git a/halo-core/src/main/java/org/xujin/halo/validator/ValidatorExecutor.java b/halo-core/src/main/java/org/xujin/halo/validator/ValidatorExecutor.java new file mode 100644 index 0000000..45bdbac --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/validator/ValidatorExecutor.java @@ -0,0 +1,30 @@ + +package org.xujin.halo.validator; + +import org.xujin.halo.extension.ExtensionExecutor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * ValidatorExecutor + * + * @author xujin 2017-11-04 + */ +@Component +public class ValidatorExecutor extends ExtensionExecutor { + + @Autowired + private PlainValidatorRepository plainValidatorRepository; + + public void validate(Class targetClz, Object candidate) { + ValidatorI validator = locateComponent(targetClz); + validator.validate(candidate); + } + + @Override + protected C locateComponent(Class targetClz) { + C validator = (C) plainValidatorRepository.getPlainValidators().get(targetClz); + return null != validator ? validator : super.locateComponent(targetClz); + } + +} \ No newline at end of file diff --git a/halo-core/src/main/java/org/xujin/halo/validator/ValidatorI.java b/halo-core/src/main/java/org/xujin/halo/validator/ValidatorI.java new file mode 100644 index 0000000..d34bf80 --- /dev/null +++ b/halo-core/src/main/java/org/xujin/halo/validator/ValidatorI.java @@ -0,0 +1,16 @@ + +package org.xujin.halo.validator; + +/** + * Validator Interface + * @author xujin 2017-11-04 + */ +public interface ValidatorI { + + /** + * Validate candidate, throw according exception if failed + * @param candidate + */ + public void validate(Object candidate); + +} diff --git a/halo-core/src/main/resources/sample.properties b/halo-core/src/main/resources/sample.properties new file mode 100644 index 0000000..e69de29 diff --git a/halo-core/src/test/java/org/xujin/halo/TestConfig.java b/halo-core/src/test/java/org/xujin/halo/TestConfig.java new file mode 100644 index 0000000..2c069d6 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/TestConfig.java @@ -0,0 +1,27 @@ +package org.xujin.halo; + +import org.xujin.halo.boot.Bootstrap; +import org.springframework.context.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * TestConfig + * + * @author xujin 2018-01-06 7:57 AM + */ +@Configuration +@ComponentScan("org.xujin.halo") +@PropertySource(value = {"/sample.properties"}) +public class TestConfig { + + @Bean(initMethod = "init") + public Bootstrap bootstrap() { + Bootstrap bootstrap = new Bootstrap(); + List packagesToScan = new ArrayList<>(); + packagesToScan.add("org.xujin.halo.test"); + bootstrap.setPackages(packagesToScan); + return bootstrap; + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/CustomerCommandTest.java b/halo-core/src/test/java/org/xujin/halo/test/CustomerCommandTest.java new file mode 100644 index 0000000..0b89b17 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/CustomerCommandTest.java @@ -0,0 +1,122 @@ +package org.xujin.halo.test; + +import org.xujin.halo.TestConfig; +import org.xujin.halo.context.TenantContext; +import org.xujin.halo.dto.Response; +import org.xujin.halo.exception.BasicErrorCode; +import org.xujin.halo.test.customer.*; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.xujin.halo.test.customer.*; + +/** + * CustomerCommandTest + * + * @author xujin 2018-01-06 7:55 PM + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = {TestConfig.class}) +public class CustomerCommandTest { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Autowired + private CustomerServiceI customerService; + + @Value("${bizCode}") + private String bizCode; + + @Before + public void setUp() { + TenantContext.set(Constants.TENANT_ID, bizCode); + } + + @Test + public void testBizOneAddCustomerSuccess(){ + //1. Prepare + TenantContext.set(Constants.TENANT_ID, Constants.BIZ_1); + AddCustomerCmd addCustomerCmd = new AddCustomerCmd(); + CustomerCO customerCO = new CustomerCO(); + customerCO.setCompanyName("xxxx"); + customerCO.setSource(Constants.SOURCE_RFQ); + customerCO.setCustomerType(CustomerType.IMPORTANT); + addCustomerCmd.setCustomerCO(customerCO); + + //2. Execute + Response response = customerService.addCustomer(addCustomerCmd); + + //3. Expect Success + Assert.assertTrue(response.isSuccess()); + } + + @Test + public void testBizOneAddCustomerFailure(){ + //1. Prepare + TenantContext.set(Constants.TENANT_ID, Constants.BIZ_1); + AddCustomerCmd addCustomerCmd = new AddCustomerCmd(); + CustomerCO customerCO = new CustomerCO(); + customerCO.setCompanyName("xxxx"); + customerCO.setSource(Constants.SOURCE_AD); + customerCO.setCustomerType(CustomerType.IMPORTANT); + addCustomerCmd.setCustomerCO(customerCO); + + //2. Execute + Response response = customerService.addCustomer(addCustomerCmd); + + //3. Expect exception + Assert.assertFalse(response.isSuccess()); + Assert.assertEquals(response.getErrCode(), BasicErrorCode.BIZ_ERROR.getErrCode()); + } + + @Test + public void testBizTwoAddCustomer(){ + //1. Prepare + TenantContext.set(Constants.TENANT_ID, Constants.BIZ_2); + AddCustomerCmd addCustomerCmd = new AddCustomerCmd(); + CustomerCO customerCO = new CustomerCO(); + customerCO.setCompanyName("xxxx"); + customerCO.setSource(Constants.SOURCE_AD); + customerCO.setCustomerType(CustomerType.IMPORTANT); + addCustomerCmd.setCustomerCO(customerCO); + + //2. Execute + Response response = customerService.addCustomer(addCustomerCmd); + + //3. Expect Success + Assert.assertTrue(response.isSuccess()); + } + + @Test + public void testCompanyTypeViolation(){ + AddCustomerCmd addCustomerCmd = new AddCustomerCmd(); + CustomerCO customerCO = new CustomerCO(); + customerCO.setCompanyName("xxxx"); + customerCO.setSource("p4p"); + customerCO.setCustomerType(CustomerType.VIP); + addCustomerCmd.setCustomerCO(customerCO); + Response response = customerService.addCustomer(addCustomerCmd); + + //Expect biz exception + Assert.assertFalse(response.isSuccess()); + Assert.assertEquals(response.getErrCode(), BasicErrorCode.BIZ_ERROR.getErrCode()); + } + + @Test + public void testParamValidationFail(){ + AddCustomerCmd addCustomerCmd = new AddCustomerCmd(); + Response response = customerService.addCustomer(addCustomerCmd); + + //Expect parameter validation error + Assert.assertFalse(response.isSuccess()); + Assert.assertEquals(response.getErrCode(), BasicErrorCode.PARAM_ERROR.getErrCode()); + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/EntityPrototypeTest.java b/halo-core/src/test/java/org/xujin/halo/test/EntityPrototypeTest.java new file mode 100644 index 0000000..6969658 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/EntityPrototypeTest.java @@ -0,0 +1,33 @@ +package org.xujin.halo.test; + +import org.xujin.halo.TestConfig; +import org.xujin.halo.common.ApplicationContextHelper; +import org.xujin.halo.test.customer.entity.CustomerEntity; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * EntityPrototypeTest + * + * @author xujin + * @date 2018-01-07 12:34 PM + */ + +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = {TestConfig.class}) +public class EntityPrototypeTest { + + @Test + public void testPrototype(){ + CustomerEntity customerEntity1 = (CustomerEntity)ApplicationContextHelper.getBean(CustomerEntity.class); + System.out.println(customerEntity1); + CustomerEntity customerEntity2 = (CustomerEntity)ApplicationContextHelper.getBean(CustomerEntity.class); + System.out.println(customerEntity2); + + Assert.assertEquals(customerEntity1, customerEntity2); + Assert.assertFalse(customerEntity1 == customerEntity2); //It should be different objects + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/SpringConfigTest.java b/halo-core/src/test/java/org/xujin/halo/test/SpringConfigTest.java new file mode 100644 index 0000000..dc73ee7 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/SpringConfigTest.java @@ -0,0 +1,17 @@ +package org.xujin.halo.test; + +import org.xujin.halo.TestConfig; +import org.xujin.halo.boot.Bootstrap; +import org.xujin.halo.boot.RegisterFactory; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +public class SpringConfigTest { + + public static void main(String[] args) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class); + Bootstrap bootstrap = (Bootstrap)context.getBean("bootstrap"); + RegisterFactory registerFactory = (RegisterFactory)context.getBean("registerFactory"); + System.out.println(registerFactory); + System.out.println(bootstrap.getPackages()); + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/AddCustomerCmd.java b/halo-core/src/test/java/org/xujin/halo/test/customer/AddCustomerCmd.java new file mode 100644 index 0000000..7245722 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/AddCustomerCmd.java @@ -0,0 +1,21 @@ +package org.xujin.halo.test.customer; + + +import org.xujin.halo.dto.Command; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +/** + * AddCustomerCmd + * + * @author xujin 2018-01-06 7:28 PM + */ +@Data +public class AddCustomerCmd extends Command { + + @NotNull + @Valid + private CustomerCO customerCO; +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/AddCustomerCmdExe.java b/halo-core/src/test/java/org/xujin/halo/test/customer/AddCustomerCmdExe.java new file mode 100644 index 0000000..b0fc03e --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/AddCustomerCmdExe.java @@ -0,0 +1,47 @@ +package org.xujin.halo.test.customer; + +import org.xujin.halo.command.Command; +import org.xujin.halo.command.CommandExecutorI; +import org.xujin.halo.dto.Response; +import org.xujin.halo.extension.ExtensionExecutor; +import org.xujin.halo.logger.Logger; +import org.xujin.halo.logger.LoggerFactory; +import org.xujin.halo.test.customer.convertor.CustomerConvertorExtPt; +import org.xujin.halo.test.customer.entity.CustomerEntity; +import org.xujin.halo.test.customer.validator.extensionpoint.AddCustomerValidatorExtPt; +import org.xujin.halo.validator.ValidatorExecutor; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * AddCustomerCmdExe + * + * @author xujin 2018-01-06 7:48 PM + */ +@Command +public class AddCustomerCmdExe implements CommandExecutorI { + + private Logger logger = LoggerFactory.getLogger(AddCustomerCmd.class); + + @Autowired + private ValidatorExecutor validatorExecutor; + + @Autowired + private ExtensionExecutor extensionExecutor; + + + @Override + public Response execute(AddCustomerCmd cmd) { + logger.info("Start processing command:" + cmd); + validatorExecutor.validate(AddCustomerValidatorExtPt.class, cmd); + + //Convert CO to Entity + CustomerEntity customerEntity = extensionExecutor.execute(CustomerConvertorExtPt.class, extension -> extension.clientToEntity(cmd.getCustomerCO())); + + //Call Domain Entity for business logic processing + logger.info("Call Domain Entity for business logic processing..."+customerEntity); + customerEntity.addNewCustomer(); + + logger.info("End processing command:" + cmd); + return Response.buildSuccess(); + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/Constants.java b/halo-core/src/test/java/org/xujin/halo/test/customer/Constants.java new file mode 100644 index 0000000..d42050a --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/Constants.java @@ -0,0 +1,22 @@ +package org.xujin.halo.test.customer; + +/** + * Constants + * + * @author xujin + * @date 2018-01-07 1:23 AM + */ +public class Constants { + + public static final String BIZ_1 = "BIZ_ONE"; + public static final String BIZ_2 = "BIZ_TWO"; + + public static final String TENANT_ID = "122344"; + + + public static final String SOURCE_AD = "Advertisement"; + public static final String SOURCE_WB = "Web site"; + public static final String SOURCE_RFQ = "Request For Quota"; + public static final String SOURCE_MARKETING = "Marketing"; + public static final String SOURCE_OFFLINE = "Off Line"; +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerCO.java b/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerCO.java new file mode 100644 index 0000000..cf73126 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerCO.java @@ -0,0 +1,21 @@ +package org.xujin.halo.test.customer; + +import org.xujin.halo.dto.ClientObject; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +/** + * CustomerCO + * + * @author xujin 2018-01-06 7:30 PM + */ +@Data +public class CustomerCO extends ClientObject{ + + @NotEmpty + private String companyName; + @NotEmpty + private String source; //advertisement, p4p, RFQ, ATM + private CustomerType customerType; //potential, intentional, important, vip +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerDO.java b/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerDO.java new file mode 100644 index 0000000..cfaf8d8 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerDO.java @@ -0,0 +1,22 @@ +package org.xujin.halo.test.customer; + +import org.xujin.halo.repository.DataObject; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * CustomerDO + * + * @author xujin + * @date 2018-01-08 1:45 PM + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class CustomerDO implements DataObject { + private String customerId; + private String memberId; + private String globalId; + private String companyName; + private String source; + private String companyType; +} \ No newline at end of file diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerServiceI.java b/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerServiceI.java new file mode 100644 index 0000000..ca1f90d --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerServiceI.java @@ -0,0 +1,14 @@ +package org.xujin.halo.test.customer; + +import org.xujin.halo.dto.Response; +import org.xujin.halo.dto.SingleResponse; + +/** + * CustomerServiceI + * + * @author xujin 2018-01-06 7:24 PM + */ +public interface CustomerServiceI { + public Response addCustomer(AddCustomerCmd addCustomerCmd); + public SingleResponse getCustomer(GetOneCustomerQry getOneCustomerQry); +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerServiceImpl.java b/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerServiceImpl.java new file mode 100644 index 0000000..45496be --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerServiceImpl.java @@ -0,0 +1,29 @@ +package org.xujin.halo.test.customer; + +import org.xujin.halo.command.CommandBusI; +import org.xujin.halo.dto.Response; +import org.xujin.halo.dto.SingleResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * CustomerServiceImpl + * + * @author xujin 2018-01-06 7:40 PM + */ +@Service +public class CustomerServiceImpl implements CustomerServiceI{ + + @Autowired + private CommandBusI commandBus; + + @Override + public Response addCustomer(AddCustomerCmd addCustomerCmd) { + return (Response)commandBus.send(addCustomerCmd); + } + + @Override + public SingleResponse getCustomer(GetOneCustomerQry getOneCustomerQry) { + return null; + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerType.java b/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerType.java new file mode 100644 index 0000000..69fee9a --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/CustomerType.java @@ -0,0 +1,13 @@ +package org.xujin.halo.test.customer; + +/** + * CustomerType + * + * @author xujin 2018-01-06 7:35 PM + */ +public enum CustomerType { + POTENTIAL, + INTENTIONAL, + IMPORTANT, + VIP; +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/GetOneCustomerQry.java b/halo-core/src/test/java/org/xujin/halo/test/customer/GetOneCustomerQry.java new file mode 100644 index 0000000..58dca2f --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/GetOneCustomerQry.java @@ -0,0 +1,15 @@ +package org.xujin.halo.test.customer; + +import org.xujin.halo.dto.Query; +import lombok.Data; + +/** + * GetOneCustomerQry + * + * @author xujin 2018-01-06 7:38 PM + */ +@Data +public class GetOneCustomerQry extends Query{ + private long customerId; + private String companyName; +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerBizOneConvertorExt.java b/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerBizOneConvertorExt.java new file mode 100644 index 0000000..cc9cc68 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerBizOneConvertorExt.java @@ -0,0 +1,35 @@ +package org.xujin.halo.test.customer.convertor; + +import org.xujin.halo.extension.Extension; +import org.xujin.halo.test.customer.Constants; +import org.xujin.halo.test.customer.CustomerCO; +import org.xujin.halo.test.customer.entity.CustomerEntity; +import org.xujin.halo.test.customer.entity.SourceType; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * CustomerBizOneConvertorExt + * + * @author xujin + * @date 2018-01-07 3:05 AM + */ +@Extension(bizCode = Constants.BIZ_1) +public class CustomerBizOneConvertorExt implements CustomerConvertorExtPt{ + + @Autowired + private CustomerConvertor customerConvertor;//Composite basic convertor to do basic conversion + + @Override + public CustomerEntity clientToEntity(CustomerCO customerCO){ + CustomerEntity customerEntity = customerConvertor.clientToEntity(customerCO); + //In this business, AD and RFQ are regarded as different source + if(Constants.SOURCE_AD.equals(customerCO.getSource())) + { + customerEntity.setSourceType(SourceType.AD); + } + if (Constants.SOURCE_RFQ.equals(customerCO.getSource())){ + customerEntity.setSourceType(SourceType.RFQ); + } + return customerEntity; + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerBizTwoConvertorExt.java b/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerBizTwoConvertorExt.java new file mode 100644 index 0000000..9202655 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerBizTwoConvertorExt.java @@ -0,0 +1,33 @@ +package org.xujin.halo.test.customer.convertor; + +import org.xujin.halo.extension.Extension; +import org.xujin.halo.test.customer.Constants; +import org.xujin.halo.test.customer.CustomerCO; +import org.xujin.halo.test.customer.entity.CustomerEntity; +import org.xujin.halo.test.customer.entity.SourceType; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * CustomerBizTwoConvertorExt + * + * @author xujin + * @date 2018-01-07 3:05 AM + */ +@Extension(bizCode = Constants.BIZ_2) +public class CustomerBizTwoConvertorExt implements CustomerConvertorExtPt{ + + @Autowired + private CustomerConvertor customerConvertor;//Composite basic convertor to do basic conversion + + @Override + public CustomerEntity clientToEntity(CustomerCO customerCO){ + CustomerEntity customerEntity = customerConvertor.clientToEntity(customerCO); + //In this business, if customers from RFQ and Advertisement are both regarded as Advertisement + if(Constants.SOURCE_AD.equals(customerCO.getSource()) || Constants.SOURCE_RFQ.equals(customerCO.getSource())) + { + customerEntity.setSourceType(SourceType.AD); + } + return customerEntity; + } + +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerConvertor.java b/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerConvertor.java new file mode 100644 index 0000000..a0cfbc7 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerConvertor.java @@ -0,0 +1,29 @@ +package org.xujin.halo.test.customer.convertor; + +import org.xujin.halo.common.ApplicationContextHelper; +import org.xujin.halo.convertor.ConvertorI; +import org.xujin.halo.test.customer.CustomerCO; +import org.xujin.halo.test.customer.entity.CustomerEntity; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * CustomerConvertor + * + * @author xujin + * @date 2018-01-07 3:08 AM + */ +@Component +public class CustomerConvertor implements ConvertorI { + + public CustomerEntity clientToEntity(Object clientObject){ + CustomerCO customerCO = (CustomerCO)clientObject; + CustomerEntity customerEntity = (CustomerEntity)ApplicationContextHelper.getBean(CustomerEntity.class); + customerEntity.setCompanyName(customerCO.getCompanyName()); + customerEntity.setCustomerType(customerCO.getCustomerType()); + return customerEntity; + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerConvertorExtPt.java b/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerConvertorExtPt.java new file mode 100644 index 0000000..5728b53 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/convertor/CustomerConvertorExtPt.java @@ -0,0 +1,17 @@ +package org.xujin.halo.test.customer.convertor; + +import org.xujin.halo.convertor.ConvertorI; +import org.xujin.halo.extension.ExtensionPointI; +import org.xujin.halo.test.customer.CustomerCO; +import org.xujin.halo.test.customer.entity.CustomerEntity; + +/** + * CustomerConvertorExtPt + * + * @author xujin + * @date 2018-01-07 2:37 AM + */ +public interface CustomerConvertorExtPt extends ConvertorI, ExtensionPointI { + + public CustomerEntity clientToEntity(CustomerCO customerCO); +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/entity/CustomerEntity.java b/halo-core/src/test/java/org/xujin/halo/test/customer/entity/CustomerEntity.java new file mode 100644 index 0000000..1b68b9e --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/entity/CustomerEntity.java @@ -0,0 +1,45 @@ +package org.xujin.halo.test.customer.entity; + +import org.xujin.halo.domain.Entity; +import org.xujin.halo.extension.ExtensionExecutor; +import org.xujin.halo.test.customer.CustomerType; +import org.xujin.halo.test.customer.entity.rule.CustomerRuleExtPt; +import org.xujin.halo.test.customer.repository.CustomerRepository; +import lombok.Data; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +/** + * Customer Entity + * + * @author xujin + * @date 2018-01-07 2:38 AM + */ +@Data +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class CustomerEntity extends Entity { + + private String companyName; + private SourceType sourceType; + private CustomerType customerType; + + @Autowired + private CustomerRepository customerRepository; + @Autowired + private ExtensionExecutor extensionExecutor; + + public CustomerEntity() { + + } + + public void addNewCustomer() { + //Add customer policy + extensionExecutor.execute(CustomerRuleExtPt.class, extension -> extension.addCustomerCheck(this)); + + //Persist customer + customerRepository.persist(this); + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/entity/SourceType.java b/halo-core/src/test/java/org/xujin/halo/test/customer/entity/SourceType.java new file mode 100644 index 0000000..92760c8 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/entity/SourceType.java @@ -0,0 +1,13 @@ +package org.xujin.halo.test.customer.entity; + +/** + * SourceType + * + * @author xujin + * @date 2018-01-07 3:02 AM + */ +public enum SourceType { + AD, //Advertisement 广告 + WB, // Web site 网站 + RFQ; // Request For Quota 询盘 +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/entity/rule/CustomerBizOneRuleExt.java b/halo-core/src/test/java/org/xujin/halo/test/customer/entity/rule/CustomerBizOneRuleExt.java new file mode 100644 index 0000000..dc458d5 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/entity/rule/CustomerBizOneRuleExt.java @@ -0,0 +1,25 @@ +package org.xujin.halo.test.customer.entity.rule; + +import org.xujin.halo.exception.BizException; +import org.xujin.halo.extension.Extension; +import org.xujin.halo.test.customer.Constants; +import org.xujin.halo.test.customer.entity.CustomerEntity; +import org.xujin.halo.test.customer.entity.SourceType; + +/** + * CustomerBizOneRuleExt + * + * @author xujin + * @date 2018-01-07 12:10 PM + */ +@Extension(bizCode = Constants.BIZ_1) +public class CustomerBizOneRuleExt implements CustomerRuleExtPt{ + + @Override + public boolean addCustomerCheck(CustomerEntity customerEntity) { + if(SourceType.AD == customerEntity.getSourceType()){ + throw new BizException("Sorry, Customer from advertisement can not be added in this period"); + } + return true; + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/entity/rule/CustomerBizTwoRuleExt.java b/halo-core/src/test/java/org/xujin/halo/test/customer/entity/rule/CustomerBizTwoRuleExt.java new file mode 100644 index 0000000..091f3e2 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/entity/rule/CustomerBizTwoRuleExt.java @@ -0,0 +1,21 @@ +package org.xujin.halo.test.customer.entity.rule; + +import org.xujin.halo.extension.Extension; +import org.xujin.halo.test.customer.Constants; +import org.xujin.halo.test.customer.entity.CustomerEntity; + +/** + * CustomerBizTwoRuleExt + * + * @author xujin + * @date 2018-01-07 12:10 PM + */ +@Extension(bizCode = Constants.BIZ_2) +public class CustomerBizTwoRuleExt implements CustomerRuleExtPt{ + + @Override + public boolean addCustomerCheck(CustomerEntity customerEntity) { + //Any Customer can be added + return true; + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/entity/rule/CustomerRuleExtPt.java b/halo-core/src/test/java/org/xujin/halo/test/customer/entity/rule/CustomerRuleExtPt.java new file mode 100644 index 0000000..163efba --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/entity/rule/CustomerRuleExtPt.java @@ -0,0 +1,22 @@ +package org.xujin.halo.test.customer.entity.rule; + +import org.xujin.halo.extension.ExtensionPointI; +import org.xujin.halo.rule.RuleI; +import org.xujin.halo.test.customer.entity.CustomerEntity; + +/** + * CustomerRuleExtPt + * + * @author xujin + * @date 2018-01-07 12:03 PM + */ +public interface CustomerRuleExtPt extends RuleI, ExtensionPointI { + + //Different business check for different biz + public boolean addCustomerCheck(CustomerEntity customerEntity); + + //Different upgrade policy for different biz + default public void customerUpgradePolicy(CustomerEntity customerEntity){ + //Nothing special + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/interceptor/ContextInterceptor.java b/halo-core/src/test/java/org/xujin/halo/test/customer/interceptor/ContextInterceptor.java new file mode 100644 index 0000000..7d16a6e --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/interceptor/ContextInterceptor.java @@ -0,0 +1,20 @@ +package org.xujin.halo.test.customer.interceptor; + +import org.xujin.halo.command.CommandInterceptorI; +import org.xujin.halo.command.PreInterceptor; +import org.xujin.halo.dto.Command; + +/** + * ContextInterceptor + * + * @author xujin + * @date 2018-01-07 1:21 AM + */ +@PreInterceptor +public class ContextInterceptor implements CommandInterceptorI { + + @Override + public void preIntercept(Command command) { + // TenantContext.set(Constants.TENANT_ID, Constants.BIZ_1); + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/interceptor/ValidationInterceptor.java b/halo-core/src/test/java/org/xujin/halo/test/customer/interceptor/ValidationInterceptor.java new file mode 100644 index 0000000..7e58f44 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/interceptor/ValidationInterceptor.java @@ -0,0 +1,36 @@ +package org.xujin.halo.test.customer.interceptor; + +import org.xujin.halo.command.CommandInterceptorI; +import org.xujin.halo.command.PreInterceptor; +import org.xujin.halo.dto.Command; +import org.xujin.halo.exception.ParamException; +import org.xujin.halo.validator.HaloMessageInterpolator; +import org.hibernate.validator.HibernateValidator; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +/** + * ValidationInterceptor + * + * @author xujin 2018-01-06 8:27 PM + */ +@PreInterceptor +public class ValidationInterceptor implements CommandInterceptorI { + + //Enable fail fast, which will improve performance + private ValidatorFactory factory = Validation.byProvider(HibernateValidator.class).configure().failFast(true) + .messageInterpolator(new HaloMessageInterpolator()).buildValidatorFactory(); + + @Override + public void preIntercept(Command command) { + Validator validator = factory.getValidator(); + Set> constraintViolations = validator.validate(command); + constraintViolations.forEach(violation -> { + throw new ParamException(violation.getPropertyPath() + " " + violation.getMessage()); + }); + } +} \ No newline at end of file diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/repository/CustomerRepository.java b/halo-core/src/test/java/org/xujin/halo/test/customer/repository/CustomerRepository.java new file mode 100644 index 0000000..d51e54b --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/repository/CustomerRepository.java @@ -0,0 +1,19 @@ +package org.xujin.halo.test.customer.repository; + +import org.xujin.halo.repository.RepositoryI; +import org.xujin.halo.test.customer.entity.CustomerEntity; +import org.springframework.stereotype.Repository; + +/** + * CustomerRepository + * + * @author xujin + * @date 2018-01-07 11:59 AM + */ +@Repository +public class CustomerRepository implements RepositoryI { + + public void persist(CustomerEntity customerEntity){ + System.out.println("Persist customer to DB : "+ customerEntity); + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/validator/extension/AddCustomerBizOneValidator.java b/halo-core/src/test/java/org/xujin/halo/test/customer/validator/extension/AddCustomerBizOneValidator.java new file mode 100644 index 0000000..5027245 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/validator/extension/AddCustomerBizOneValidator.java @@ -0,0 +1,26 @@ +package org.xujin.halo.test.customer.validator.extension; + +import org.xujin.halo.exception.BizException; +import org.xujin.halo.extension.Extension; +import org.xujin.halo.test.customer.AddCustomerCmd; +import org.xujin.halo.test.customer.Constants; +import org.xujin.halo.test.customer.CustomerType; +import org.xujin.halo.test.customer.validator.extensionpoint.AddCustomerValidatorExtPt; + +/** + * AddCustomerBizOneValidator + * + * @author xujin + * @date 2018-01-07 1:31 AM + */ +@Extension(bizCode = Constants.BIZ_1) +public class AddCustomerBizOneValidator implements AddCustomerValidatorExtPt { + + @Override + public void validate(Object candidate) { + AddCustomerCmd addCustomerCmd = (AddCustomerCmd) candidate; + //For BIZ TWO CustomerTYpe could not be VIP + if(CustomerType.VIP == addCustomerCmd.getCustomerCO().getCustomerType()) + throw new BizException("Customer Type could not be VIP for Biz One"); + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/validator/extension/AddCustomerBizTwoValidator.java b/halo-core/src/test/java/org/xujin/halo/test/customer/validator/extension/AddCustomerBizTwoValidator.java new file mode 100644 index 0000000..8c8764f --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/validator/extension/AddCustomerBizTwoValidator.java @@ -0,0 +1,25 @@ +package org.xujin.halo.test.customer.validator.extension; + +import org.xujin.halo.exception.ParamException; +import org.xujin.halo.extension.Extension; +import org.xujin.halo.test.customer.AddCustomerCmd; +import org.xujin.halo.test.customer.Constants; +import org.xujin.halo.test.customer.validator.extensionpoint.AddCustomerValidatorExtPt; + +/** + * AddCustomerBizTwoValidator + * + * @author xujin + * @date 2018-01-07 1:31 AM + */ +@Extension(bizCode = Constants.BIZ_2) +public class AddCustomerBizTwoValidator implements AddCustomerValidatorExtPt { + + @Override + public void validate(Object candidate) { + AddCustomerCmd addCustomerCmd = (AddCustomerCmd) candidate; + //For BIZ TWO CustomerTYpe could not be null + if (addCustomerCmd.getCustomerCO().getCustomerType() == null) + throw new ParamException("CustomerType could not be null"); + } +} diff --git a/halo-core/src/test/java/org/xujin/halo/test/customer/validator/extensionpoint/AddCustomerValidatorExtPt.java b/halo-core/src/test/java/org/xujin/halo/test/customer/validator/extensionpoint/AddCustomerValidatorExtPt.java new file mode 100644 index 0000000..8b2f642 --- /dev/null +++ b/halo-core/src/test/java/org/xujin/halo/test/customer/validator/extensionpoint/AddCustomerValidatorExtPt.java @@ -0,0 +1,14 @@ +package org.xujin.halo.test.customer.validator.extensionpoint; + +import org.xujin.halo.extension.ExtensionPointI; +import org.xujin.halo.validator.ValidatorI; + +/** + * AddCustomerValidatorExtPt + * + * @author xujin + * @date 2018-01-07 1:27 AM + */ +public interface AddCustomerValidatorExtPt extends ValidatorI, ExtensionPointI { + +} diff --git a/halo-core/src/test/resources/sample.properties b/halo-core/src/test/resources/sample.properties new file mode 100644 index 0000000..9c24605 --- /dev/null +++ b/halo-core/src/test/resources/sample.properties @@ -0,0 +1 @@ +bizCode=BIZ_ONE diff --git a/halo-event/pom.xml b/halo-event/pom.xml new file mode 100644 index 0000000..d97a655 --- /dev/null +++ b/halo-event/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + org.xujin.halo + halo + 1.0.4 + ../pom.xml + + + halo-event + halo-event + + + + org.xujin.halo + halo-common + ${project.parent.version} + + + org.springframework + spring-context + provided + + + org.springframework.boot + spring-boot-autoconfigure + provided + true + + + org.slf4j + slf4j-api + + + org.apache.commons + commons-lang3 + + + diff --git a/halo-event/src/main/java/org/xujin/halo/event/EventPublisher.java b/halo-event/src/main/java/org/xujin/halo/event/EventPublisher.java new file mode 100644 index 0000000..ef5e4ff --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/EventPublisher.java @@ -0,0 +1,15 @@ +package org.xujin.halo.event; + +/** + * 事件发布器 + */ +public interface EventPublisher { + + /** + * 发布事件 + * + * @param event 事件 + */ + void publish(Object event); + +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/annotation/BizListener.java b/halo-event/src/main/java/org/xujin/halo/event/annotation/BizListener.java new file mode 100644 index 0000000..babec15 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/annotation/BizListener.java @@ -0,0 +1,24 @@ +package org.xujin.halo.event.annotation; + +import org.xujin.halo.event.annotation.listener.Listener; +import org.xujin.halo.event.extension.support.BizListenerType; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 业务监听器 + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Listener(type = BizListenerType.class) +public @interface BizListener { + + /** + * 优先级 + * (具体执行顺序需要结合@Listen注解的priorityAsc属性共同决定) + */ + @AliasFor(annotation = Listener.class, attribute = "priority") + int priority() default Integer.MAX_VALUE; +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/annotation/Listen.java b/halo-event/src/main/java/org/xujin/halo/event/annotation/Listen.java new file mode 100644 index 0000000..930dd22 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/annotation/Listen.java @@ -0,0 +1,25 @@ +package org.xujin.halo.event.annotation; + +import org.xujin.halo.event.extension.support.ClassListenResolver; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 监听 + */ +@Documented +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@org.xujin.halo.event.annotation.listener.Listen(resolver = ClassListenResolver.class) +public @interface Listen { + + /** + * 是否按照优先级升序 + *

+ * true:表示升序,即监听器中优先级值越小优先级越高;false:表示降序,即监听器中优先级值越大优先级越高。默认为升序。 + * 当一个事件发布时,总是先执行完优先级为升序的监听方法,再执行优先级为降序的监听方法 + */ + @AliasFor(annotation = org.xujin.halo.event.annotation.listener.Listen.class, attribute = "priorityAsc") + boolean priorityAsc() default true; +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/annotation/listener/Listen.java b/halo-event/src/main/java/org/xujin/halo/event/annotation/listener/Listen.java new file mode 100644 index 0000000..f910b5b --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/annotation/listener/Listen.java @@ -0,0 +1,28 @@ + +package org.xujin.halo.event.annotation.listener; + +import org.xujin.halo.event.extension.ListenResolver; + +import java.lang.annotation.*; + +/** + * 监听 + */ +@Documented +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Listen { + + /** + * 监听解决器 + */ + Class resolver(); + + /** + * 是否按照优先级升序 + *

+ * true:表示升序,即监听器中优先级值越小优先级越高;false:表示降序,即监听器中优先级值越大优先级越高。默认为升序。 + * 当一个事件发布时,总是先执行完优先级为升序的监听方法,再执行优先级为降序的监听方法 + */ + boolean priorityAsc() default true; +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/annotation/listener/Listener.java b/halo-event/src/main/java/org/xujin/halo/event/annotation/listener/Listener.java new file mode 100644 index 0000000..4cc809f --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/annotation/listener/Listener.java @@ -0,0 +1,27 @@ +package org.xujin.halo.event.annotation.listener; + +import org.xujin.halo.event.extension.ListenerType; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * 监听器 + */ +@Documented +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface Listener { + + /** + * 类型 + */ + Class type(); + + /** + * 优先级 + * (具体执行顺序需要结合@Listen注解的priorityAsc属性共同决定) + */ + int priority() default Integer.MAX_VALUE; +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/boot/EventBusAutoConfiguration.java b/halo-event/src/main/java/org/xujin/halo/event/boot/EventBusAutoConfiguration.java new file mode 100644 index 0000000..f6657da --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/boot/EventBusAutoConfiguration.java @@ -0,0 +1,14 @@ +package org.xujin.halo.event.boot; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 事件总线自动配置类 + */ +@Configuration +@Import(EventBusConfiguration.class) +public class EventBusAutoConfiguration { + // 事件总线由EventBusConfiguration进行配置 + // 本配置类的作用就是在spring-boot项目中自动导入EventBusConfiguration +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/boot/EventBusConfiguration.java b/halo-event/src/main/java/org/xujin/halo/event/boot/EventBusConfiguration.java new file mode 100644 index 0000000..4f29cf9 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/boot/EventBusConfiguration.java @@ -0,0 +1,27 @@ +package org.xujin.halo.event.boot; + +import org.xujin.halo.event.EventPublisher; +import org.xujin.halo.event.bus.EventBusesHolder; +import org.xujin.halo.event.extension.support.BizListenerType; +import org.xujin.halo.event.listener.ListenersHolder; +import org.xujin.halo.event.publisher.DefaultEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Import; + +/** + * 事件总线配置类 + * (非spring-boot项目需手动引入本配置类完成事件总线配置) + */ +@Configuration +@Import({EventBusesHolder.class, ListenersHolder.class}) +public class EventBusConfiguration { + + // 业务事件发布器 + @Bean + @DependsOn("EventBusesHolder") // 保证出现循环引用时不会出错 + public EventPublisher eventPublisher(EventBusesHolder eventBusesHolder) { + return new DefaultEventPublisher(eventBusesHolder.getEventBus(BizListenerType.class)); + } +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/bus/EventBus.java b/halo-event/src/main/java/org/xujin/halo/event/bus/EventBus.java new file mode 100644 index 0000000..fda61ec --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/bus/EventBus.java @@ -0,0 +1,82 @@ +package org.xujin.halo.event.bus; + +import org.xujin.halo.event.extension.EventTypeResolver; +import org.xujin.halo.event.listener.ListenerExecutor; + +import java.util.*; + +/** + * 事件总线 + */ +public class EventBus { + // 监听器执行器 + private List listenerExecutors = new ArrayList<>(); + // 监听器执行器缓存(key:事件类型) + private Map> listenerExecutorsCache = new HashMap<>(); + // 事件类型解决器 + private EventTypeResolver resolver; + + public EventBus(EventTypeResolver resolver) { + this.resolver = resolver; + } + + /** + * 注册监听器 + * + * @param listenerExecutor 监听器执行器 + */ + public void register(ListenerExecutor listenerExecutor) { + listenerExecutors.add(listenerExecutor); + Collections.sort(listenerExecutors); + refreshListenerCache(); + } + + /** + * 分派事件 + * (先执行优先级升序,再执行优先级降序) + * + * @param event 事件 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + public void dispatch(Object event) throws Throwable { + // 获取该事件类型的监听器缓存 + List theListenerExecutors = listenerExecutorsCache.get(resolver.resolve(event)); + if (theListenerExecutors != null) { + // 执行监听器 + for (ListenerExecutor listenerExecutor : theListenerExecutors) { + listenerExecutor.execute(event); + } + } + } + + // 刷新监听器缓存 + private void refreshListenerCache() { + listenerExecutorsCache = new HashMap<>(); + // 获取本总线所有的事件类型 + Set eventTypes = new HashSet<>(); + for (ListenerExecutor listenerExecutor : listenerExecutors) { + eventTypes.addAll(listenerExecutor.getEventTypes(true)); + eventTypes.addAll(listenerExecutor.getEventTypes(false)); + } + // 根据事件类型设置缓存 + for (Object eventType : eventTypes) { + // 特定事件类型的监听器缓存 + List theListenerExecutors = new ArrayList<>(); + // 获取指定事件类型的升序监听器 + for (ListenerExecutor listenerExecutor : listenerExecutors) { + if (listenerExecutor.getEventTypes(true).contains(eventType)) { + theListenerExecutors.add(listenerExecutor); + } + } + // 获取指定事件类型的降序监听器 + for (int i = listenerExecutors.size() - 1; i >= 0; i--) { + ListenerExecutor listenerExecutor = listenerExecutors.get(i); + if (listenerExecutor.getEventTypes(false).contains(eventType)) { + theListenerExecutors.add(listenerExecutor); + } + } + // 设置缓存 + listenerExecutorsCache.put(eventType, theListenerExecutors); + } + } +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/bus/EventBusesHolder.java b/halo-event/src/main/java/org/xujin/halo/event/bus/EventBusesHolder.java new file mode 100644 index 0000000..fe54435 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/bus/EventBusesHolder.java @@ -0,0 +1,48 @@ +package org.xujin.halo.event.bus; + +import org.xujin.halo.event.extension.ListenerType; +import org.xujin.halo.event.listener.ListenerExecutor; +import org.xujin.halo.event.listener.ListenerParser; +import org.xujin.halo.event.listener.ListenersHolder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; + +/** + * 事件总线持有器 + */ +@Component +public class EventBusesHolder { + @Autowired + private ListenersHolder listenersHolder; + // 事件总线Map(key:总线类型) + private Map eventBusMap = new HashMap<>(); + + // 初始化(根据监听器类型创建相应类型的事件总线,spring自动执行) + @PostConstruct + public void init() { + for (Class type : listenersHolder.getTypes()) { + // 初始化事件总线 + EventBus eventBus = getEventBus(type); + for (ListenerExecutor listenerExecutor : listenersHolder.getRequiredListenerExecutors(type)) { + eventBus.register(listenerExecutor); + } + } + } + + /** + * 获取事件总线 + * (如果不存在该类型的事件总线,则新创建一个) + * + * @param type 总线类型 + */ + public synchronized EventBus getEventBus(Class type) { + if (!eventBusMap.containsKey(type)) { + eventBusMap.put(type, new EventBus(ListenerParser.parseEventTypeResolver(type))); + } + return eventBusMap.get(type); + } +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/extension/EventTypeResolver.java b/halo-event/src/main/java/org/xujin/halo/event/extension/EventTypeResolver.java new file mode 100644 index 0000000..1f43064 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/extension/EventTypeResolver.java @@ -0,0 +1,15 @@ +package org.xujin.halo.event.extension; + +/** + * 事件类型解决器 + */ +public interface EventTypeResolver { + + /** + * 根据事件得到对应的事件类型 + * + * @param event 事件 + * @return 事件类型 + */ + Object resolve(Object event); +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/extension/ListenResolver.java b/halo-event/src/main/java/org/xujin/halo/event/extension/ListenResolver.java new file mode 100644 index 0000000..79fa5f0 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/extension/ListenResolver.java @@ -0,0 +1,29 @@ +package org.xujin.halo.event.extension; + +import java.lang.reflect.Method; + +/** + * 监听解决器 + */ +public interface ListenResolver { + + /** + * 初始化(每个实例仅调用一次) + * + * @param listenMethod 监听方法 + */ + void init(Method listenMethod); + + /** + * 获取监听的事件类型 + */ + Object getEventType(); + + /** + * 解决调用监听方法的入参 + * + * @param event 事件 + * @return 调用监听方法的入参 + */ + Object[] resolveArgs(Object event); +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/extension/ListenerType.java b/halo-event/src/main/java/org/xujin/halo/event/extension/ListenerType.java new file mode 100644 index 0000000..5ae5320 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/extension/ListenerType.java @@ -0,0 +1,12 @@ +package org.xujin.halo.event.extension; + +/** + * 监听器类型 + */ +public interface ListenerType { + + /** + * 获取事件类型解决器 + */ + EventTypeResolver getResolver(); +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/extension/support/BizListenerType.java b/halo-event/src/main/java/org/xujin/halo/event/extension/support/BizListenerType.java new file mode 100644 index 0000000..7d544d5 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/extension/support/BizListenerType.java @@ -0,0 +1,15 @@ +package org.xujin.halo.event.extension.support; + +import org.xujin.halo.event.extension.EventTypeResolver; +import org.xujin.halo.event.extension.ListenerType; + +/** + * biz监听器类型 + */ +public class BizListenerType implements ListenerType { + + @Override + public EventTypeResolver getResolver() { + return ClassEventTypeResolver.INSTANCE; + } +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/extension/support/ClassEventTypeResolver.java b/halo-event/src/main/java/org/xujin/halo/event/extension/support/ClassEventTypeResolver.java new file mode 100644 index 0000000..ba560b9 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/extension/support/ClassEventTypeResolver.java @@ -0,0 +1,18 @@ +package org.xujin.halo.event.extension.support; + +import org.xujin.halo.event.extension.EventTypeResolver; + +/** + * Class事件类型解决器(事件类型就是事件对应的Class类) + */ +public class ClassEventTypeResolver implements EventTypeResolver { + /** + * 实例 + */ + public static final ClassEventTypeResolver INSTANCE = new ClassEventTypeResolver(); + + @Override + public Object resolve(Object event) { + return event.getClass(); + } +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/extension/support/ClassListenResolver.java b/halo-event/src/main/java/org/xujin/halo/event/extension/support/ClassListenResolver.java new file mode 100644 index 0000000..b35b954 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/extension/support/ClassListenResolver.java @@ -0,0 +1,35 @@ +package org.xujin.halo.event.extension.support; + +import org.xujin.halo.event.extension.ListenResolver; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Method; + +/** + * Class监听解决器(监听方法只能有一个入参,事件类型就是入参的Class类) + */ +public class ClassListenResolver implements ListenResolver { + // 监听的事件类型 + private Class eventType; + + @Override + public void init(Method listenMethod) { + // 校验入参 + Class[] parameterTypes = listenMethod.getParameterTypes(); + if (parameterTypes.length != 1) { + throw new IllegalArgumentException(String.format("监听方法%s必须只有一个入参", ClassUtils.getQualifiedMethodName(listenMethod))); + } + // 设置事件类型 + eventType = parameterTypes[0]; + } + + @Override + public Object getEventType() { + return eventType; + } + + @Override + public Object[] resolveArgs(Object event) { + return new Object[]{event}; + } +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/listener/ListenerExecutor.java b/halo-event/src/main/java/org/xujin/halo/event/listener/ListenerExecutor.java new file mode 100644 index 0000000..e145ee9 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/listener/ListenerExecutor.java @@ -0,0 +1,149 @@ +package org.xujin.halo.event.listener; + +import org.xujin.halo.event.extension.EventTypeResolver; +import org.xujin.halo.event.extension.ListenResolver; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.xujin.halo.method.MethodExecutor; + +/** + * 监听器执行器 + */ +public class ListenerExecutor implements Comparable { + // 监听器类型 + private Class type; + // 优先级 + private int priority; + // 监听器 + private Object listener; + // 事件类型解决器 + private EventTypeResolver resolver; + // 监听执行器map(key:被监听的事件类型) + private Map listenExecutorMap = new HashMap<>(); + + public ListenerExecutor(Class type, int priority, Object listener, EventTypeResolver resolver) { + this.type = type; + this.priority = priority; + this.listener = listener; + this.resolver = resolver; + } + + /** + * 执行监听事件 + * + * @param event 事件 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + public void execute(Object event) throws Throwable { + ListenExecutor listenExecutor = listenExecutorMap.get(resolver.resolve(event)); + if (listenExecutor != null) { + listenExecutor.execute(listener, event); + } + } + + /** + * 添加监听执行器 + * + * @param listenExecutor 监听执行器 + * @throws IllegalStateException 如果已存在相同事件类型的监听执行器 + */ + public void addListenExecutor(ListenExecutor listenExecutor) { + if (listenExecutorMap.containsKey(listenExecutor.getEventType())) { + throw new IllegalStateException("监听器" + ClassUtils.getShortName(listener.getClass()) + "存在多个监听" + listenExecutor.getEventType() + "事件的方法"); + } + listenExecutorMap.put(listenExecutor.getEventType(), listenExecutor); + } + + /** + * 获取监听器类型 + */ + public Class getType() { + return type; + } + + /** + * 获取监听器 + */ + public Object getListener() { + return listener; + } + + /** + * 获取指定优先级顺序的监听事件类型 + * + * @param priorityAsc 是否优先级升序(true:升序,false:降序) + */ + public Set getEventTypes(boolean priorityAsc) { + Set eventTypes = new HashSet<>(); + for (Object eventType : listenExecutorMap.keySet()) { + ListenExecutor listenExecutor = listenExecutorMap.get(eventType); + if (listenExecutor.isPriorityAsc() == priorityAsc) { + eventTypes.add(eventType); + } + } + return eventTypes; + } + + @Override + public int compareTo(ListenerExecutor listenerExecutor) { + return priority - listenerExecutor.priority; + } + + /** + * 校验监听器执行器是否有效 + * + * @throws IllegalStateException 如果校验不通过 + */ + public void validate() { + if (listener == null || type == null || resolver == null) { + throw new IllegalStateException("监听器内部要素不全"); + } + } + + /** + * 监听执行器 + */ + public static class ListenExecutor extends MethodExecutor { + // 监听解决器 + private ListenResolver resolver; + // 是否优先级升序 + private boolean priorityAsc; + + public ListenExecutor(ListenResolver resolver, boolean priorityAsc, Method targetMethod) { + super(targetMethod); + this.resolver = resolver; + this.priorityAsc = priorityAsc; + } + + /** + * 执行监听 + * + * @param listener 监听器 + * @param event 事件 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + public void execute(Object listener, Object event) throws Throwable { + execute(listener, resolver.resolveArgs(event)); + } + + /** + * 获取事件类型 + */ + public Object getEventType() { + return resolver.getEventType(); + } + + /** + * 是否是优先级升序 + */ + public boolean isPriorityAsc() { + return priorityAsc; + } + } +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/listener/ListenerParser.java b/halo-event/src/main/java/org/xujin/halo/event/listener/ListenerParser.java new file mode 100644 index 0000000..2b1ce12 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/listener/ListenerParser.java @@ -0,0 +1,79 @@ +package org.xujin.halo.event.listener; + +import org.xujin.halo.event.annotation.listener.Listen; +import org.xujin.halo.event.annotation.listener.Listener; +import org.xujin.halo.event.extension.EventTypeResolver; +import org.xujin.halo.event.extension.ListenResolver; +import org.xujin.halo.event.extension.ListenerType; +import org.xujin.halo.event.listener.ListenerExecutor.ListenExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.cglib.core.ReflectUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * 监听器解析器 + */ +public class ListenerParser { + // 日志记录器 + private static final Logger logger = LoggerFactory.getLogger(ListenerParser.class); + + /** + * 解析监听器 + * + * @param listener 监听器 + * @return 监听器执行器 + */ + public static ListenerExecutor parseListener(Object listener) { + // 获取目标class(应对AOP代理情况) + Class listenerClass = AopUtils.getTargetClass(listener); + logger.debug("解析监听器:{}", ClassUtils.getQualifiedName(listenerClass)); + // 此处得到的@Listener是已经经过@AliasFor属性别名进行属性同步后的结果 + Listener listenerAnnotation = AnnotatedElementUtils.findMergedAnnotation(listenerClass, Listener.class); + // 创建监听器执行器 + ListenerExecutor listenerExecutor = new ListenerExecutor(listenerAnnotation.type(), listenerAnnotation.priority(), listener, parseEventTypeResolver(listenerAnnotation.type())); + for (Method method : listenerClass.getDeclaredMethods()) { + Listen listenAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Listen.class); + if (listenAnnotation != null) { + ListenExecutor listenExecutor = parseListen(listenAnnotation, method); + listenerExecutor.addListenExecutor(listenExecutor); + } + } + listenerExecutor.validate(); + + return listenerExecutor; + } + + /** + * 通过监听器类型解析得到事件类型解决器 + * + * @param clazz 监听器类型 + */ + public static EventTypeResolver parseEventTypeResolver(Class clazz) { + ListenerType listenerType = (ListenerType) ReflectUtils.newInstance(clazz); + return listenerType.getResolver(); + } + + // 解析监听方法 + private static ListenExecutor parseListen(Listen listenAnnotation, Method method) { + logger.debug("解析监听方法:{}", method); + // 校验方法类型 + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException("监听方法" + ClassUtils.getQualifiedMethodName(method) + "必须是public类型"); + } + // 校验返回类型 + if (method.getReturnType() != void.class) { + throw new IllegalArgumentException("监听方法" + ClassUtils.getQualifiedMethodName(method) + "的返回必须是void"); + } + // 创建监听解决器 + ListenResolver resolver = (ListenResolver) ReflectUtils.newInstance(listenAnnotation.resolver()); + resolver.init(method); + + return new ListenExecutor(resolver, listenAnnotation.priorityAsc(), method); + } +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/listener/ListenersHolder.java b/halo-event/src/main/java/org/xujin/halo/event/listener/ListenersHolder.java new file mode 100644 index 0000000..3a1d01a --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/listener/ListenersHolder.java @@ -0,0 +1,58 @@ +package org.xujin.halo.event.listener; + +import org.xujin.halo.event.annotation.listener.Listener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; + +import javax.annotation.PostConstruct; +import java.util.*; + +/** + * 监听器持有器 + */ +@Component +public class ListenersHolder { + @Autowired + private ApplicationContext applicationContext; + // 监听器执行器Map(key:监听器的类型) + private Map> listenerExecutorsMap = new HashMap<>(); + + // 初始化(查询spring容器中所有的@Listener监听器并解析,spring自动执行) + @PostConstruct + public void init() { + String[] beanNames = applicationContext.getBeanNamesForAnnotation(Listener.class); + for (String beanName : beanNames) { + // 解析监听器 + ListenerExecutor listenerExecutor = ListenerParser.parseListener(applicationContext.getBean(beanName)); + // 将执行器放入持有器中 + List listenerExecutors = listenerExecutorsMap.get(listenerExecutor.getType()); + if (listenerExecutors == null) { + listenerExecutors = new ArrayList<>(); + listenerExecutorsMap.put(listenerExecutor.getType(), listenerExecutors); + } + listenerExecutors.add(listenerExecutor); + } + } + + /** + * 获取所有的监听器类型 + */ + public Set getTypes() { + return listenerExecutorsMap.keySet(); + } + + /** + * 获取指定类型的监听器执行器 + * + * @param type 监听器类型 + * @throws IllegalArgumentException 如果不存在该类型的监听器执行器 + */ + public List getRequiredListenerExecutors(Class type) { + if (!listenerExecutorsMap.containsKey(type)) { + throw new IllegalArgumentException("不存在" + ClassUtils.getShortName(type) + "类型的监听器"); + } + return listenerExecutorsMap.get(type); + } +} diff --git a/halo-event/src/main/java/org/xujin/halo/event/publisher/DefaultEventPublisher.java b/halo-event/src/main/java/org/xujin/halo/event/publisher/DefaultEventPublisher.java new file mode 100644 index 0000000..0797533 --- /dev/null +++ b/halo-event/src/main/java/org/xujin/halo/event/publisher/DefaultEventPublisher.java @@ -0,0 +1,26 @@ +package org.xujin.halo.event.publisher; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.xujin.halo.event.EventPublisher; +import org.xujin.halo.event.bus.EventBus; + +/** + * 事件发布器默认实现类 + */ +public class DefaultEventPublisher implements EventPublisher { + // 事件总线 + private EventBus eventBus; + + public DefaultEventPublisher(EventBus eventBus) { + this.eventBus = eventBus; + } + + @Override + public void publish(Object event) { + try { + eventBus.dispatch(event); + } catch (Throwable e) { + ExceptionUtils.rethrow(e); + } + } +} diff --git a/halo-event/src/main/resources/META-INF/spring.factories b/halo-event/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..9f0b09f --- /dev/null +++ b/halo-event/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.xujin.halo.event.boot.EventBusAutoConfiguration \ No newline at end of file diff --git a/halo-flow/pom.xml b/halo-flow/pom.xml new file mode 100644 index 0000000..2157355 --- /dev/null +++ b/halo-flow/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + org.xujin.halo + halo + 1.0.4 + + + halo-flow + halo-flow + + + + org.xujin.halo + halo-common + ${project.parent.version} + + + org.xujin.halo + halo-event + ${project.parent.version} + + + org.springframework + spring-context + provided + + + org.springframework + spring-tx + provided + + + org.springframework.boot + spring-boot-autoconfigure + provided + true + + + org.apache.commons + commons-lang3 + + + diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/FlowEngine.java b/halo-flow/src/main/java/org/xujin/halo/flow/FlowEngine.java new file mode 100644 index 0000000..3b27ca7 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/FlowEngine.java @@ -0,0 +1,49 @@ +package org.xujin.halo.flow; + +import java.util.Map; + +/** + * 流程引擎 + */ +public interface FlowEngine { + + /** + * 执行流程 + * + * @param flow 流程名称 + * @param target 目标对象 + * @return 流程执行结束后的目标对象(可能和传入的目标对象不是同一个对象) + */ + T start(String flow, T target); + + /** + * 执行流程 + * + * @param flow 流程名称 + * @param target 目标对象 + * @param attachment 附件(为null的话则会自动生成一个空Map作为附件) + * @return 流程执行结束后的目标对象(可能和传入的目标对象不是同一个对象) + */ + T start(String flow, T target, Map attachment); + + /** + * 以新事务插入目标对象到数据库并提交事务 + * + * @param flow 流程名称 + * @param target 目标对象 + * @param attachment 附件(为null的话则会自动生成一个空Map作为附件) + * @return 插入到数据库后的目标对象(可能和传入的目标对象不是同一个对象) + */ + T insertTarget(String flow, T target, Map attachment); + + /** + * 以新事务插入目标对象到数据库并提交事务,然后执行流程 + * + * @param flow 流程名称 + * @param target 目标对象 + * @param attachment 附件(为null的话则会自动生成一个空Map作为附件) + * @return 流程执行结束后的目标对象(可能和传入的目标对象不是同一个对象) + */ + T insertTargetAndStart(String flow, T target, Map attachment); + +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/EndNode.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/EndNode.java new file mode 100644 index 0000000..5631fa5 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/EndNode.java @@ -0,0 +1,24 @@ +package org.xujin.halo.flow.annotation.flow; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 结束节点 + * (每个流程必须至少有一个结束节点,它是流程结束的标志。当流程跳转到结束节点时,流程会自动结束; + * 对应的节点决策器不能有入参,且返回类型必须是void,决策器的方法体不会被执行) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Node(autoExecute = false, newTx = true) +public @interface EndNode { + + /** + * 节点名称(默认使用被注解的函数名,在一个流程内节点名称需唯一) + */ + @AliasFor(annotation = Node.class, attribute = "name") + String name() default ""; + +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/Flow.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/Flow.java new file mode 100644 index 0000000..18883da --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/Flow.java @@ -0,0 +1,34 @@ +package org.xujin.halo.flow.annotation.flow; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * 流程 + *

+ * 流程包含的节点类型:开始节点(@StartNode)、状态节点(@StateNode)、处理节点(@ProcessNode)、等待节点(@WaitNode)、结束节点(@EndNode) + * 一:开启了流程事务情况 + * 流程在刚开始执行时会自动的开启一个新事务并调用流程事务锁住目标对象; + * 当流程被正常中断或正常执行结束(无异常抛出),则会提交事务;否则如果有任何异常抛出,则会回滚事务(当然已经提交的那些事务是不会回滚的)。 + *

+ * 二:未开启流程事务情况 + * 整个执行过程中流程引擎不会对事务做任何操作(既不会主动开启事务,也不会主动提交事务),也不会调用流程事务锁住目标对象;@StateNode节点效果也会跟@ProcessNode节点效果一样 + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface Flow { + + /** + * 流程名称(默认使用被注解的类名,首字母小写) + */ + String name() default ""; + + /** + * 是否开启流程事务(默认开启) + */ + boolean enableFlowTx() default true; + +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/Node.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/Node.java new file mode 100644 index 0000000..29b5aaa --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/Node.java @@ -0,0 +1,34 @@ +package org.xujin.halo.flow.annotation.flow; + +import java.lang.annotation.*; + +/** + * 节点 + * (此为节点父注解,StartNode、ProcessNode、StateNode、WaitNode、EndNode都是根据此注解延伸的) + */ +@Documented +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Node { + + /** + * 节点名称(默认使用被注解的函数名,在一个流程内节点名称需唯一) + */ + String name() default ""; + + /** + * 节点处理器(默认不执行处理器) + */ + String processor() default ""; + + /** + * 是否自动执行本节点(默认自动执行) + */ + boolean autoExecute() default true; + + /** + * 本节点执行前是否创建新事务(默认不新建) + */ + boolean newTx() default false; + +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/ProcessNode.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/ProcessNode.java new file mode 100644 index 0000000..51a97ad --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/ProcessNode.java @@ -0,0 +1,30 @@ +package org.xujin.halo.flow.annotation.flow; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 处理节点 + * (处理节点是一种单纯的处理单元,即使开启了流程事务情况,此类型节点在执行前也不会提交事务; + * 对应的节点决策器返回值类型必须为String,入参类型可为:()、(TargetContext)、(T)、(T, TargetContext)————T表示能被对应的处理器返回结果赋值的类型) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Node +public @interface ProcessNode { + + /** + * 节点名称(默认使用被注解的函数名,在一个流程内节点名称需唯一) + */ + @AliasFor(annotation = Node.class, attribute = "name") + String name() default ""; + + /** + * 节点处理器(默认不执行处理器) + */ + @AliasFor(annotation = Node.class, attribute = "processor") + String processor() default ""; + +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/StartNode.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/StartNode.java new file mode 100644 index 0000000..4fdc643 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/StartNode.java @@ -0,0 +1,30 @@ +package org.xujin.halo.flow.annotation.flow; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 开始节点 + * (每个流程都必须有一个唯一的开始节点,开始节点应该是一个流程象的最开始执行的节点; + * 对应的节点决策器返回值类型必须为String,入参类型可为:()、(TargetContext)、(T)、(T, TargetContext)————T表示能被对应的处理器返回结果赋值的类型) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Node(newTx = true) +public @interface StartNode { + + /** + * 节点名称(默认使用被注解的函数名,在一个流程内节点名称需唯一) + */ + @AliasFor(annotation = Node.class, attribute = "name") + String name() default ""; + + /** + * 节点处理器(默认不执行处理器) + */ + @AliasFor(annotation = Node.class, attribute = "processor") + String processor() default ""; + +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/StateNode.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/StateNode.java new file mode 100644 index 0000000..2ab207b --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/StateNode.java @@ -0,0 +1,30 @@ +package org.xujin.halo.flow.annotation.flow; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 状态节点 + * (对于开启了流程事务情况,状态节点是一个状态开始的标志(需要新事务来执行),也是上一个状态结束的标志(需要提交老事务),所以在状态节点执行前会先提交事务然后开启新事务并锁住目标对象; + * 对应的节点决策器返回值类型必须为String,入参类型可为:()、(TargetContext)、(T)、(T, TargetContext)————T表示能被对应的处理器返回结果赋值的类型) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Node(newTx = true) +public @interface StateNode { + + /** + * 节点名称(默认使用被注解的函数名,在一个流程内节点名称需唯一) + */ + @AliasFor(annotation = Node.class, attribute = "name") + String name() default ""; + + /** + * 节点处理器(默认不执行处理器) + */ + @AliasFor(annotation = Node.class, attribute = "processor") + String processor() default ""; + +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/TargetMapping.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/TargetMapping.java new file mode 100644 index 0000000..490c480 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/TargetMapping.java @@ -0,0 +1,15 @@ +package org.xujin.halo.flow.annotation.flow; + +import java.lang.annotation.*; + +/** + * 目标对象映射 + *

+ * 流程开启执行前会自动调用此注解对应的方法,将目标对象映射到开始执行的流程节点。 + * 每次开启新事务时都会调用它将目标对象映射到流程节点 + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface TargetMapping { +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/WaitNode.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/WaitNode.java new file mode 100644 index 0000000..cc44038 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/flow/WaitNode.java @@ -0,0 +1,30 @@ +package org.xujin.halo.flow.annotation.flow; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 等待节点 + * (只有当等待节点是第一个被执行的节点,等待节点才会被执行;否则流程执行到等待节点时会正常中断。(这符合等待异步通知这类场景)。 + * 对应的节点决策器返回值类型必须为String,入参类型可为:()、(TargetContext)、(T)、(T, TargetContext)————T表示能被对应的处理器返回结果赋值的类型) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Node(autoExecute = false, newTx = true) +public @interface WaitNode { + + /** + * 节点名称(默认使用被注解的函数名,在一个流程内节点名称需唯一) + */ + @AliasFor(annotation = Node.class, attribute = "name") + String name() default ""; + + /** + * 节点处理器(默认不执行处理器) + */ + @AliasFor(annotation = Node.class, attribute = "processor") + String processor() default ""; + +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/FlowListener.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/FlowListener.java new file mode 100644 index 0000000..36ce3d5 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/FlowListener.java @@ -0,0 +1,24 @@ +package org.xujin.halo.flow.annotation.listener; + +import org.xujin.halo.event.annotation.listener.Listener; +import org.xujin.halo.flow.listener.FlowListenerType; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 流程监听器 + * (监听的是所有流程发生的事件) + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Listener(type = FlowListenerType.class) +public @interface FlowListener { + + /** + * 优先级 + */ + @AliasFor(annotation = Listener.class, attribute = "priority") + int priority() default Integer.MAX_VALUE; +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/ListenFlowException.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/ListenFlowException.java new file mode 100644 index 0000000..00b6972 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/ListenFlowException.java @@ -0,0 +1,25 @@ +package org.xujin.halo.flow.annotation.listener; + +import org.xujin.halo.event.annotation.listener.Listen; +import org.xujin.halo.flow.listener.ListenFlowExceptionResolver; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 监听流程异常事件 + * (配合TheFlowListener一起使用;当流程执行过程中发生任何异常,都会调用注入本注解对应的方法; + * 一个TheFlowListener内最多只能出现一次;对应的监听方法入参类型必须为(Throwable, TargetContext)) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Listen(resolver = ListenFlowExceptionResolver.class) +public @interface ListenFlowException { + + /** + * 是否按照优先级升序 + */ + @AliasFor(annotation = Listen.class, attribute = "priorityAsc") + boolean priorityAsc() default true; +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/ListenNodeDecided.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/ListenNodeDecided.java new file mode 100644 index 0000000..34a5dba --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/ListenNodeDecided.java @@ -0,0 +1,25 @@ +package org.xujin.halo.flow.annotation.listener; + +import org.xujin.halo.event.annotation.listener.Listen; +import org.xujin.halo.flow.listener.ListenNodeDecidedResolver; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 监听节点选择事件 + * (配合TheFlowListener一起使用;流程执行过程中,当每次发生节点选择事件时都会调用注入本注解对应的方法; + * 一个TheFlowListener内最多只能出现一次;对应的监听方法入参必须为(String, TargetContext)) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Listen(resolver = ListenNodeDecidedResolver.class) +public @interface ListenNodeDecided { + + /** + * 是否按照优先级升序 + */ + @AliasFor(annotation = Listen.class, attribute = "priorityAsc") + boolean priorityAsc() default true; +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/TheFlowListener.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/TheFlowListener.java new file mode 100644 index 0000000..bd29b45 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/listener/TheFlowListener.java @@ -0,0 +1,29 @@ +package org.xujin.halo.flow.annotation.listener; + +import org.xujin.halo.event.annotation.listener.Listener; +import org.xujin.halo.flow.listener.TheFlowListenerType; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 特定流程监听器 + * (监听的是某一个特定流程发生的事件,配合@ListenNodeDecide、@ListenFlowException一起使用) + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Listener(type = TheFlowListenerType.class) +public @interface TheFlowListener { + + /** + * 被监听的流程 + */ + String flow(); + + /** + * 优先级 + */ + @AliasFor(annotation = Listener.class, attribute = "priority") + int priority() default Integer.MAX_VALUE; +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/Processor.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/Processor.java new file mode 100644 index 0000000..a787ed6 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/Processor.java @@ -0,0 +1,27 @@ +package org.xujin.halo.flow.annotation.processor; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * 节点处理器 + *

+ * 执行步骤: + * 1、@ProcessorBefore(可选) + * 2、@ProcessorExecute(必需) + * 3、@ProcessorAfter(可选) + * 4、@ProcessorError(可选,如果@ProcessorBefore、@ProcessorExecute、@ProcessorAfter任何一个发生异常则执行@ProcessorError) + * 5、@ProcessorEnd(可选,无论是否发生异常都会执行) + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface Processor { + + /** + * 处理器名字(默认使用被注解类的名字,首字母小写) + */ + String name() default ""; +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorAfter.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorAfter.java new file mode 100644 index 0000000..978f1a7 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorAfter.java @@ -0,0 +1,15 @@ +package org.xujin.halo.flow.annotation.processor; + +import java.lang.annotation.*; + +/** + * 后置处理(可选) + *

+ * 一般是业务处理后的收尾工作。 + * 入参必须是TargetContext类型,返回值必须是void(比如:void after(TargetContext targetContext)) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ProcessorAfter { +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorBefore.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorBefore.java new file mode 100644 index 0000000..702377f --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorBefore.java @@ -0,0 +1,15 @@ +package org.xujin.halo.flow.annotation.processor; + +import java.lang.annotation.*; + +/** + * 前置处理(可选) + *

+ * 一般进行预处理,比如一些预校验。 + * 入参必须是TargetContext类型,返回值必须是void(比如:void before(TargetContext targetContext)) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ProcessorBefore { +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorEnd.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorEnd.java new file mode 100644 index 0000000..dfa784d --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorEnd.java @@ -0,0 +1,15 @@ +package org.xujin.halo.flow.annotation.processor; + +import java.lang.annotation.*; + +/** + * 结束处理(可选) + *

+ * 无论是否发生异常都会执行. + * 入参必须是TargetContext类型,返回值必须是void(比如:void end(TargetContext targetContext)) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ProcessorEnd { +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorError.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorError.java new file mode 100644 index 0000000..bb20300 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorError.java @@ -0,0 +1,15 @@ +package org.xujin.halo.flow.annotation.processor; + +import java.lang.annotation.*; + +/** + * 错误处理(可选) + *

+ * 在执行@ProcessorBefore、@ProcessorExecute、@ProcessorAfter任何一个发生异常时会执行。 + * 入参必须是TargetContext类型,返回值必须是void(比如:void error(TargetContext targetContext)) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ProcessorError { +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorExecute.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorExecute.java new file mode 100644 index 0000000..b005588 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/processor/ProcessorExecute.java @@ -0,0 +1,16 @@ +package org.xujin.halo.flow.annotation.processor; + +import java.lang.annotation.*; + +/** + * 业务处理(必需) + *

+ * 此注解对应的方法返回值会作为处理器的返回值返回给流程节点。 + * 入参必须是TargetContext类型;对于返回参数类型的话只要是能被赋值给节点决策器的入参就行 + * (比如:String execute(TargetContext targetContext)) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ProcessorExecute { +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/transaction/FlowTx.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/transaction/FlowTx.java new file mode 100644 index 0000000..15bfacc --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/transaction/FlowTx.java @@ -0,0 +1,22 @@ +package org.xujin.halo.flow.annotation.transaction; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * 流程事务 + * (一个流程最多能有一个流程事务类) + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface FlowTx { + + /** + * 对应的流程 + */ + String flow(); + +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/transaction/InsertTarget.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/transaction/InsertTarget.java new file mode 100644 index 0000000..c705e3f --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/transaction/InsertTarget.java @@ -0,0 +1,17 @@ +package org.xujin.halo.flow.annotation.transaction; + +import java.lang.annotation.*; + +/** + * 插入目标对象到数据库 + *

+ * 本注解的作用:创建一个新事务用来插入目标对象到数据库并提交,前提是调用流程引擎的insertTargetAndStart方法。 + * 本注解存在的原因:在开启流程事务情况下,流程引擎是创建新事务进行执行,如果插入目标对象到数据库的事务未提交就执行流程引擎,则流程引擎在锁目标对象时就会出现死锁。 + * 所以流程引擎留一个口子新开启一个事务来插入目标对象到数据库。 + * 注意:如果插入目标对象时发现目标对象在数据库中已存在,根据幂等性原则,应该使用数据库中的已存在的目标对象(流程引擎是通过锁目标对象来实现)) + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InsertTarget { +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/annotation/transaction/LockTarget.java b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/transaction/LockTarget.java new file mode 100644 index 0000000..8b13141 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/annotation/transaction/LockTarget.java @@ -0,0 +1,15 @@ +package org.xujin.halo.flow.annotation.transaction; + +import java.lang.annotation.*; + +/** + * 锁目标对象 + *

+ * 流程引擎每次开启新事务时都会调用@LockTarget类型方法,用于锁住目标对象 + * 注意:锁住目标对象后,应该更新目标对象到目标上下文 + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface LockTarget { +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/boot/FlowEngineAutoConfiguration.java b/halo-flow/src/main/java/org/xujin/halo/flow/boot/FlowEngineAutoConfiguration.java new file mode 100644 index 0000000..b026792 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/boot/FlowEngineAutoConfiguration.java @@ -0,0 +1,17 @@ +package org.xujin.halo.flow.boot; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.xujin.halo.event.boot.EventBusAutoConfiguration; + +/** + * 流程引擎自动配置类 + */ +@Configuration +@AutoConfigureAfter(EventBusAutoConfiguration.class) +@Import(FlowEngineConfiguration.class) +public class FlowEngineAutoConfiguration { + // 流程引擎由FlowEngineConfiguration进行配置 + // 本配置类的作用就是在spring-boot项目中自动导入FlowEngineConfiguration +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/boot/FlowEngineConfiguration.java b/halo-flow/src/main/java/org/xujin/halo/flow/boot/FlowEngineConfiguration.java new file mode 100644 index 0000000..9bbdd39 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/boot/FlowEngineConfiguration.java @@ -0,0 +1,33 @@ +package org.xujin.halo.flow.boot; + +import org.xujin.halo.event.boot.EventBusConfiguration; +import org.xujin.halo.flow.FlowEngine; +import org.xujin.halo.flow.engine.DefaultFlowEngine; +import org.xujin.halo.flow.flow.FlowsHolder; +import org.xujin.halo.flow.listener.DefaultFlowListener; +import org.xujin.halo.flow.processor.ProcessorsHolder; +import org.xujin.halo.flow.transaction.FlowTxsHolder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Import; + +/** + * 流程引擎配置类 + * (非spring-boot项目需手动引入本配置类完成流程引擎配置) + */ +@Configuration +@Import({EventBusConfiguration.class, + FlowsHolder.class, + ProcessorsHolder.class, + FlowTxsHolder.class, + DefaultFlowListener.class}) +public class FlowEngineConfiguration { + + // 流程引擎 + @Bean + @DependsOn({"FlowsHolder", "FlowTxsHolder"}) // 保证出现循环引用时不会出错 + public FlowEngine flowEngine(FlowsHolder flowsHolder, FlowTxsHolder flowTxsHolder) { + return new DefaultFlowEngine(flowsHolder, flowTxsHolder); + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/engine/DefaultFlowEngine.java b/halo-flow/src/main/java/org/xujin/halo/flow/engine/DefaultFlowEngine.java new file mode 100644 index 0000000..0747b2b --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/engine/DefaultFlowEngine.java @@ -0,0 +1,101 @@ +package org.xujin.halo.flow.engine; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.xujin.halo.flow.FlowEngine; +import org.xujin.halo.flow.flow.FlowExecutor; +import org.xujin.halo.flow.flow.FlowsHolder; +import org.xujin.halo.flow.transaction.FlowTxExecutor; +import org.xujin.halo.flow.transaction.FlowTxsHolder; +import org.springframework.util.ClassUtils; + +import java.util.Map; + +/** + * 流程引擎默认实现类 + */ +public class DefaultFlowEngine implements FlowEngine { + // 流程持有器 + private FlowsHolder flowsHolder; + // 流程事务持有器 + private FlowTxsHolder flowTxsHolder; + + public DefaultFlowEngine(FlowsHolder flowsHolder, FlowTxsHolder flowTxsHolder) { + this.flowsHolder = flowsHolder; + this.flowTxsHolder = flowTxsHolder; + } + + @Override + public T start(String flow, T target) { + return start(flow, target, null); + } + + @Override + public T start(String flow, T target, Map attachment) { + // 校验目标对象类型 + checkClassOfTarget(target, flow); + // 构造目标上下文 + TargetContext targetContext = new TargetContext(target, attachment); + // 执行流程 + executeFlow(flow, targetContext); + + return targetContext.getTarget(); + } + + @Override + public T insertTarget(String flow, T target, Map attachment) { + // 校验目标对象类型 + checkClassOfTarget(target, flow); + // 构造目标上下文 + TargetContext targetContext = new TargetContext(target, attachment); + // 执行插入目标对象 + executeInsertTarget(flow, targetContext); + + return targetContext.getTarget(); + } + + @Override + public T insertTargetAndStart(String flow, T target, Map attachment) { + // 校验目标对象类型 + checkClassOfTarget(target, flow); + // 构造目标上下文 + TargetContext targetContext = new TargetContext(target, attachment); + // 执行插入目标对象 + executeInsertTarget(flow, targetContext); + // 执行流程 + executeFlow(flow, targetContext); + + return targetContext.getTarget(); + } + + // 校验目标对象类型 + private void checkClassOfTarget(Object target, String flow) { + FlowExecutor flowExecutor = flowsHolder.getRequiredFlowExecutor(flow); + if (!flowExecutor.getClassOfTarget().isAssignableFrom(target.getClass())) { + throw new IllegalArgumentException(String.format("传入的目标对象的类型[%s]和流程%s期望的类型[%s]不匹配", ClassUtils.getShortName(target.getClass()), flowExecutor.getFlowName(), ClassUtils.getShortName(flowExecutor.getClassOfTarget()))); + } + } + + // 执行插入目标对象 + private void executeInsertTarget(String flow, TargetContext targetContext) { + try { + // 获取流程事务执行器 + FlowTxExecutor flowTxExecutor = flowTxsHolder.getRequiredFlowTxExecutor(flow); + // 插入目标对象 + flowTxExecutor.insertTarget(targetContext); + } catch (Throwable e) { + ExceptionUtils.rethrow(e); + } + } + + // 执行流程 + private void executeFlow(String flow, TargetContext targetContext) { + try { + // 获取流程执行器 + FlowExecutor flowExecutor = flowsHolder.getRequiredFlowExecutor(flow); + // 执行流程 + flowExecutor.execute(targetContext); + } catch (Throwable e) { + ExceptionUtils.rethrow(e); + } + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/engine/TargetContext.java b/halo-flow/src/main/java/org/xujin/halo/flow/engine/TargetContext.java new file mode 100644 index 0000000..c0786b5 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/engine/TargetContext.java @@ -0,0 +1,56 @@ +package org.xujin.halo.flow.engine; + +import org.springframework.util.Assert; + +import java.util.HashMap; +import java.util.Map; + +/** + * 目标上下文 + */ +public class TargetContext { + // 目标对象 + private T target; + // 附件(一般存的是target不包含的信息,但在流程执行中又需要用到的信息) + private Map attachment; + + public TargetContext(T target, Map attachment) { + Assert.notNull(target, "目标对象不能为null"); + this.target = target; + this.attachment = attachment; + if (this.attachment == null) { + this.attachment = new HashMap<>(); + } + } + + /** + * 获取目标对象 + */ + public T getTarget() { + return target; + } + + /** + * 刷新目标对象 + * + * @param target 目标对象(会替换掉目标上下文中原有的目标对象) + */ + public void refreshTarget(T target) { + Assert.notNull(target, "目标对象不能为null"); + this.target = target; + } + + /** + * 获取附件属性 + */ + public V getAttachmentAttr(Object key) { + return (V) attachment.get(key); + } + + /** + * 设置附件属性 + */ + public void setAttachmentAttr(Object key, Object value) { + attachment.put(key, value); + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/event/FlowExceptionEvent.java b/halo-flow/src/main/java/org/xujin/halo/flow/event/FlowExceptionEvent.java new file mode 100644 index 0000000..5b6ded0 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/event/FlowExceptionEvent.java @@ -0,0 +1,42 @@ +package org.xujin.halo.flow.event; + +import org.xujin.halo.flow.engine.TargetContext; + +/** + * 流程异常事件 + */ +public class FlowExceptionEvent { + // 流程名称 + private String flow; + // 发生的异常 + private Throwable throwable; + // 目标上下文 + private TargetContext targetContext; + + public FlowExceptionEvent(String flow, Throwable throwable, TargetContext targetContext) { + this.flow = flow; + this.throwable = throwable; + this.targetContext = targetContext; + } + + /** + * 获取流程名称 + */ + public String getFlow() { + return flow; + } + + /** + * 获取发生的异常 + */ + public Throwable getThrowable() { + return throwable; + } + + /** + * 获取目标上下文 + */ + public TargetContext getTargetContext() { + return targetContext; + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/event/NodeDecidedEvent.java b/halo-flow/src/main/java/org/xujin/halo/flow/event/NodeDecidedEvent.java new file mode 100644 index 0000000..b6e24c9 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/event/NodeDecidedEvent.java @@ -0,0 +1,42 @@ +package org.xujin.halo.flow.event; + +import org.xujin.halo.flow.engine.TargetContext; + +/** + * 节点选择事件 + */ +public class NodeDecidedEvent { + // 流程名称 + private String flow; + // 被选择的节点 + private String node; + // 目标上下文 + private TargetContext targetContext; + + public NodeDecidedEvent(String flow, String node, TargetContext targetContext) { + this.flow = flow; + this.node = node; + this.targetContext = targetContext; + } + + /** + * 获取流程名称 + */ + public String getFlow() { + return flow; + } + + /** + * 获取被选择的节点 + */ + public String getNode() { + return node; + } + + /** + * 获取目标上下文 + */ + public TargetContext getTargetContext() { + return targetContext; + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/flow/FlowExecutor.java b/halo-flow/src/main/java/org/xujin/halo/flow/flow/FlowExecutor.java new file mode 100644 index 0000000..e637f47 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/flow/FlowExecutor.java @@ -0,0 +1,467 @@ +package org.xujin.halo.flow.flow; + +import org.xujin.halo.event.EventPublisher; +import org.xujin.halo.flow.engine.TargetContext; +import org.xujin.halo.flow.event.FlowExceptionEvent; +import org.xujin.halo.flow.event.NodeDecidedEvent; +import org.xujin.halo.flow.processor.ProcessorExecutor; +import org.xujin.halo.flow.transaction.FlowTxExecutor; +import org.springframework.util.ClassUtils; +import org.xujin.halo.method.MethodExecutor; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 流程执行器 + */ +public class FlowExecutor { + // 流程名称 + private String flowName; + // 是否开启流程事务 + private boolean enableFlowTx; + // 流程 + private Object flow; + // 开始节点 + private String startNode; + // 结束节点 + private Set endNodes = new HashSet<>(); + // 节点执行器Map(key:节点名称) + private Map nodeExecutorMap = new HashMap<>(); + // 目标对象映射执行器 + private TargetMappingExecutor mappingExecutor; + // 流程事务执行器 + private FlowTxExecutor flowTxExecutor; + // 事件发布器 + private EventPublisher eventPublisher; + + public FlowExecutor(String flowName, boolean enableFlowTx, Object flow, EventPublisher eventPublisher) { + this.flowName = flowName; + this.enableFlowTx = enableFlowTx; + this.flow = flow; + this.eventPublisher = eventPublisher; + } + + /** + * 执行流程 + * + * @param targetContext 目标上下文 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + public void execute(TargetContext targetContext) throws Throwable { + try { + // 获取即将执行的节点 + String node = beforeStep(targetContext); + if (!endNodes.contains(node)) { + // 获取节点执行器 + NodeExecutor nodeExecutor = nodeExecutorMap.get(node); + do { + // 执行节点 + node = nodeExecutor.execute(flow, targetContext); + // 判断是否中断流程 + if (node == null) { + break; + } + // 判断节点是否存在 + if (!nodeExecutorMap.containsKey(node)) { + throw new RuntimeException("流程" + flowName + "不存在节点" + node); + } + // 发送节点选择事件 + eventPublisher.publish(new NodeDecidedEvent(flowName, node, targetContext)); + // 获取下一个节点执行器 + nodeExecutor = nodeExecutorMap.get(node); + // 判断是否提交事务 + if (enableFlowTx && nodeExecutor.isNewTx() && nodeExecutor.isAutoExecute()) { + afterStep(); + // 刷新即将执行的节点(防止事务提交后目标对象被其他线程抢占被执行到其他节点,此处就是更新到最新节点) + node = beforeStep(targetContext); + nodeExecutor = nodeExecutorMap.get(node); + } + } while (nodeExecutor.isAutoExecute()); + } + afterStep(); + } catch (Throwable e) { + afterThrowing(e, targetContext); + throw e; + } + } + + // 在每一个步骤执行前执行 + private String beforeStep(TargetContext targetContext) throws Throwable { + if (enableFlowTx) { + // 创建事务 + flowTxExecutor.createTx(); + // 锁目标对象 + flowTxExecutor.lockTarget(targetContext); + } + // 返回接下来需要执行的节点 + return targetMappingToNode(targetContext); + } + + // 在每一个步骤执行后执行 + private void afterStep() { + if (enableFlowTx) { + // 提交事务 + flowTxExecutor.commitTx(); + } + } + + // 在发生异常后执行 + private void afterThrowing(Throwable throwable, TargetContext targetContext) { + try { + if (enableFlowTx) { + // 回滚事务 + flowTxExecutor.rollbackTx(); + } + } finally { + // 发送流程异常事件 + eventPublisher.publish(new FlowExceptionEvent(flowName, throwable, targetContext)); + } + } + + // 目标对象映射到节点 + private String targetMappingToNode(TargetContext targetContext) throws Throwable { + // 执行映射执行器 + String node = mappingExecutor.execute(flow, targetContext); + if (!nodeExecutorMap.containsKey(node)) { + throw new RuntimeException("流程" + flowName + "不存在节点" + node); + } + + return node; + } + + /** + * 添加节点 + * + * @param nodeExecutor 节点执行器 + * @throws IllegalStateException 如果存在同名的节点 + */ + public void addNode(NodeExecutor nodeExecutor) { + if (nodeExecutorMap.containsKey(nodeExecutor.getNodeName())) { + throw new IllegalStateException("流程" + flowName + "存在同名的节点" + nodeExecutor.getNodeName()); + } + nodeExecutorMap.put(nodeExecutor.getNodeName(), nodeExecutor); + } + + /** + * 设置开始节点 + * + * @throws IllegalStateException 如果开始节点已存在 + */ + public void setStartNode(String startNode) { + if (this.startNode != null) { + throw new IllegalStateException("流程" + flowName + "存在多个开始节点"); + } + this.startNode = startNode; + } + + /** + * 添加结束节点 + */ + public void addEndNode(String endNode) { + endNodes.add(endNode); + } + + /** + * 设置目标对象映射执行器 + * + * @param mappingExecutor 目标对象映射执行器 + * @throws IllegalStateException 如果目标对象映射执行器已经被设置过 + */ + public void setMappingExecutor(TargetMappingExecutor mappingExecutor) { + if (this.mappingExecutor != null) { + throw new IllegalStateException("流程" + flowName + "存在多个目标对象映射方法(@TargetMapping类型方法)"); + } + this.mappingExecutor = mappingExecutor; + } + + /** + * 设置流程事务执行器 + * + * @param flowTxExecutor 流程事务执行器 + * @throws IllegalStateException 如果流程事务执行器不能被设置或已经被设置 + */ + public void setFlowTxExecutor(FlowTxExecutor flowTxExecutor) { + if (!enableFlowTx) { + throw new IllegalStateException("流程" + flowName + "的enableFlowTx属性为关闭状态,不能设置流程事务"); + } + if (this.flowTxExecutor != null) { + throw new IllegalStateException("流程" + flowName + "的流程事务执行器已被设置,不能重复设置"); + } + this.flowTxExecutor = flowTxExecutor; + } + + /** + * 获取流程名称 + */ + public String getFlowName() { + return flowName; + } + + /** + * 获取流程 + */ + public Object getFlow() { + return flow; + } + + /** + * 获取目标对象类型 + */ + public Class getClassOfTarget() { + return mappingExecutor.getClassOfTarget(); + } + + /** + * 校验流程执行器是否有效 + * + * @throws IllegalStateException 如果校验不通过 + */ + public void validate() { + if (flowName == null || flow == null || eventPublisher == null) { + throw new IllegalStateException("流程" + flowName + "内部要素不全"); + } + if (startNode == null) { + throw new IllegalStateException("流程" + flowName + "缺少开始节点"); + } + if (endNodes.isEmpty()) { + throw new IllegalStateException("流程" + flowName + "没有结束节点"); + } + if (mappingExecutor == null) { + throw new IllegalStateException("流程" + flowName + "缺少目标对象映射方法(@TargetMapping类型方法)"); + } + if (enableFlowTx) { + if (flowTxExecutor == null) { + throw new IllegalStateException("流程" + flowName + "的enableFlowTx属性为开启状态,但未设置对应的流程事务"); + } + } else { + if (flowTxExecutor != null) { + throw new IllegalStateException("流程" + flowName + "的enableFlowTx属性为关闭状态,但设置了流程事务"); + } + } + // 校验流程节点的处理器的目标对象类型是否匹配 + for (NodeExecutor nodeExecutor : nodeExecutorMap.values()) { + Class classOfTargetOfProcessor = nodeExecutor.getClassOfTargetOfProcessor(); + if (classOfTargetOfProcessor != null && !classOfTargetOfProcessor.isAssignableFrom(getClassOfTarget())) { + throw new IllegalStateException("流程" + flowName + "内" + nodeExecutor.getNodeName() + "节点的处理器的目标对象类型和流程的目标对象类型不匹配"); + } + Class classOfTargetOfNodeDecider = nodeExecutor.getClassOfTargetOfNodeDecider(); + if (classOfTargetOfNodeDecider != null && classOfTargetOfNodeDecider != getClassOfTarget()) { + throw new IllegalStateException("流程" + flowName + "内目标对象类型不统一"); + } + } + // 校验流程事务的目标对象类型是否匹配 + if (flowTxExecutor != null) { + if (!flowTxExecutor.getClassOfTarget().isAssignableFrom(getClassOfTarget())) { + throw new IllegalStateException("流程事务" + ClassUtils.getShortName(flowTxExecutor.getClass()) + "的目标对象类型与流程" + flowName + "的目标对象类型不匹配"); + } + } + } + + /** + * 节点执行器 + */ + public static class NodeExecutor { + // 节点名称 + private String nodeName; + // 处理器执行器 + private ProcessorExecutor processorExecutor; + // 是否自动执行本节点 + private boolean autoExecute; + // 本节点执行前是否创建新事务 + private boolean newTx; + // 节点决策器执行器 + private NodeDeciderExecutor nodeDeciderExecutor; + + public NodeExecutor(String nodeName, ProcessorExecutor processorExecutor, boolean autoExecute, boolean newTx) { + this.nodeName = nodeName; + this.processorExecutor = processorExecutor; + this.autoExecute = autoExecute; + this.newTx = newTx; + } + + /** + * 执行节点 + * + * @param flow 流程 + * @param targetContext 目标上下文 + * @return 下个节点 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + public String execute(Object flow, TargetContext targetContext) throws Throwable { + Object processResult = null; + if (processorExecutor != null) { + // 执行节点处理器 + processResult = processorExecutor.execute(targetContext); + } + // 执行节点决策器 + return nodeDeciderExecutor.execute(flow, processResult, targetContext); + } + + /** + * 设置节点决策器执行器 + */ + public void setNodeDeciderExecutor(NodeDeciderExecutor nodeDeciderExecutor) { + if (this.nodeDeciderExecutor != null) { + throw new IllegalStateException("节点" + nodeName + "已设置节点决策器执行器,不能重复设置"); + } + this.nodeDeciderExecutor = nodeDeciderExecutor; + } + + /** + * 本节点是否自动执行 + */ + public boolean isAutoExecute() { + return autoExecute; + } + + /** + * 本节点执行前是否创建新事务 + */ + public boolean isNewTx() { + return newTx; + } + + /** + * 获取节点名称 + */ + public String getNodeName() { + return nodeName; + } + + /** + * 获取处理器的目标对象类型 + * + * @return null 如果该节点没有处理器 + */ + public Class getClassOfTargetOfProcessor() { + return processorExecutor != null ? processorExecutor.getClassOfTarget() : null; + } + + /** + * 获取节点决策器的目标对象类型 + * + * @return null 如果节点决策器没有TargetContext参数 + */ + public Class getClassOfTargetOfNodeDecider() { + return nodeDeciderExecutor.getClassOfTarget(); + } + + /** + * 校验节点执行器是否有效 + * + * @throws IllegalStateException 如果校验不通过 + */ + public void validate() { + if (nodeName == null || nodeDeciderExecutor == null) { + throw new IllegalStateException("节点" + nodeName + "内部要素不全"); + } + } + + /** + * 节点决策器执行器(选出下一个节点) + */ + public static class NodeDeciderExecutor extends MethodExecutor { + // 参数类型 + private ParametersType parametersType; + // 目标对象类型 + private Class classOfTarget; + + public NodeDeciderExecutor(Method targetMethod, ParametersType parametersType, Class classOfTarget) { + super(targetMethod); + this.parametersType = parametersType; + this.classOfTarget = classOfTarget; + } + + /** + * 执行 + * + * @param flow 流程 + * @param processResult 处理器执行结果 + * @param targetContext 目标上下文 + * @return 下个节点名称 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + public String execute(Object flow, Object processResult, TargetContext targetContext) throws Throwable { + switch (parametersType) { + case NONE: + return (String) execute(flow, new Object[]{}); + case ONLY_PROCESS_RESULT: + return (String) execute(flow, new Object[]{processResult}); + case ONLY_TARGET_CONTEXT: + return (String) execute(flow, new Object[]{targetContext}); + case PROCESS_RESULT_AND_TARGET_CONTEXT: + return (String) execute(flow, new Object[]{processResult, targetContext}); + default: + throw new IllegalStateException("下个节点选择方法执行器内部状态不对"); + } + } + + /** + * 获取目标对象类型 + * + * @return null 如果节点决策器没有TargetContext参数 + */ + public Class getClassOfTarget() { + return classOfTarget; + } + + /** + * 下个节点选择方法参数类型 + */ + public enum ParametersType { + /** + * 无参数 + */ + NONE, + /** + * 只有处理结果参数 + */ + ONLY_PROCESS_RESULT, + /** + * 只有目标上下文 + */ + ONLY_TARGET_CONTEXT, + /** + * 处理结果和目标上下文都有 + */ + PROCESS_RESULT_AND_TARGET_CONTEXT,; + } + } + } + + /** + * 目标对象映射执行器 + */ + public static class TargetMappingExecutor extends MethodExecutor { + // 目标对象类型 + private Class classOfTarget; + + public TargetMappingExecutor(Method targetMethod, Class classOfTarget) { + super(targetMethod); + this.classOfTarget = classOfTarget; + } + + /** + * 执行节点映射方法 + * + * @param flow 流程 + * @param targetContext 目标上下文 + * @return 映射到的流程节点名称 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + public String execute(Object flow, TargetContext targetContext) throws Throwable { + return (String) execute(flow, new Object[]{targetContext.getTarget()}); + } + + /** + * 获取目标对象类型 + */ + public Class getClassOfTarget() { + return classOfTarget; + } + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/flow/FlowParser.java b/halo-flow/src/main/java/org/xujin/halo/flow/flow/FlowParser.java new file mode 100644 index 0000000..e51ea3d --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/flow/FlowParser.java @@ -0,0 +1,186 @@ +package org.xujin.halo.flow.flow; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.ClassUtils; +import org.xujin.halo.event.bus.EventBusesHolder; +import org.xujin.halo.event.publisher.DefaultEventPublisher; +import org.xujin.halo.flow.annotation.flow.*; +import org.xujin.halo.flow.engine.TargetContext; +import org.xujin.halo.flow.listener.FlowListenerType; +import org.xujin.halo.flow.processor.ProcessorExecutor; +import org.xujin.halo.flow.processor.ProcessorsHolder; +import org.xujin.halo.flow.transaction.FlowTxsHolder; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * 流程解析器 + */ +public class FlowParser { + // 日志记录器 + private static final Logger logger = LoggerFactory.getLogger(FlowParser.class); + + /** + * @param flow 流程 + * @param processorsHolder 处理器持有器 + * @param flowTxsHolder 流程事务持有器 + * @param eventBusesHolder 事件总线持有器 + * @return 流程执行器 + */ + public static FlowExecutor parseFlow(Object flow, ProcessorsHolder processorsHolder, FlowTxsHolder flowTxsHolder, EventBusesHolder eventBusesHolder) { + // 获取目标class(应对AOP代理情况) + Class flowClass = AopUtils.getTargetClass(flow); + logger.debug("解析流程:{}", ClassUtils.getQualifiedName(flowClass)); + Flow flowAnnotation = flowClass.getAnnotation(Flow.class); + // 获取流程名称 + String flowName = flowAnnotation.name(); + if (StringUtils.isEmpty(flowName)) { + flowName = ClassUtils.getShortNameAsProperty(flowClass); + } + // 新建流程执行器 + FlowExecutor flowExecutor = new FlowExecutor(flowName, flowAnnotation.enableFlowTx(), flow, new DefaultEventPublisher(eventBusesHolder.getEventBus(FlowListenerType.class))); + if (flowAnnotation.enableFlowTx()) { + flowExecutor.setFlowTxExecutor(flowTxsHolder.getRequiredFlowTxExecutor(flowName)); + } + for (Method method : flowClass.getDeclaredMethods()) { + // 此处得到的@Node是已经经过@AliasFor属性别名进行属性同步后的结果 + Node nodeAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Node.class); + if (nodeAnnotation != null) { + // 解析节点 + FlowExecutor.NodeExecutor nodeExecutor = parseNode(nodeAnnotation, method, processorsHolder); + // 添加节点 + flowExecutor.addNode(nodeExecutor); + // 校验是否为开始节点 + if (method.isAnnotationPresent(StartNode.class)) { + flowExecutor.setStartNode(nodeExecutor.getNodeName()); + } + // 校验是否为结束节点 + if (method.isAnnotationPresent(EndNode.class)) { + flowExecutor.addEndNode(nodeExecutor.getNodeName()); + } + } else if (method.isAnnotationPresent(TargetMapping.class)) { + // 设置目标对象映射执行器 + flowExecutor.setMappingExecutor(parseTargetMapping(method)); + } + } + flowExecutor.validate(); + + return flowExecutor; + } + + // 解析节点 + private static FlowExecutor.NodeExecutor parseNode(Node nodeAnnotation, Method method, ProcessorsHolder processorsHolder) { + logger.debug("解析流程节点:node={},method={}", nodeAnnotation, method); + // 获取节点名称 + String nodeName = nodeAnnotation.name(); + if (StringUtils.isEmpty(nodeName)) { + nodeName = method.getName(); + } + // 获取处理器 + ProcessorExecutor processorExecutor = null; + if (StringUtils.isNotEmpty(nodeAnnotation.processor())) { + processorExecutor = processorsHolder.getRequiredProcessorExecutor(nodeAnnotation.processor()); + } + // 新建节点执行器 + FlowExecutor.NodeExecutor nodeExecutor = new FlowExecutor.NodeExecutor(nodeName, processorExecutor, nodeAnnotation.autoExecute(), nodeAnnotation.newTx()); + // 设置节点决策器执行器 + nodeExecutor.setNodeDeciderExecutor(parseNodeDecider(method, processorExecutor)); + nodeExecutor.validate(); + + return nodeExecutor; + } + + // 解析节点决策器 + private static FlowExecutor.NodeExecutor.NodeDeciderExecutor parseNodeDecider(Method method, ProcessorExecutor processorExecutor) { + logger.debug("解析节点决策器:{}", method); + // 校验方法类型 + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException("解析节点决策器" + ClassUtils.getQualifiedMethodName(method) + "必须是public类型"); + } + // 判断+校验入参类型,可以存在的入参类型:()、(TargetContext)、(T)、(T, TargetContext)————T表示能被处理器返回结果赋值的类型 + FlowExecutor.NodeExecutor.NodeDeciderExecutor.ParametersType parametersType; + Class classOfTarget = null; + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length == 0) { + // 入参类型:() + parametersType = FlowExecutor.NodeExecutor.NodeDeciderExecutor.ParametersType.NONE; + } else { + if (method.isAnnotationPresent(EndNode.class)) { + throw new IllegalArgumentException("结束节点的决策器" + ClassUtils.getQualifiedMethodName(method) + "不能有入参"); + } + if (parameterTypes.length == 1) { + if (parameterTypes[0] == TargetContext.class) { + // 入参类型:(TargetContext) + parametersType = FlowExecutor.NodeExecutor.NodeDeciderExecutor.ParametersType.ONLY_TARGET_CONTEXT; + // 解析目标对象类型 + ResolvableType resolvableType = ResolvableType.forMethodParameter(method, 0); + classOfTarget = resolvableType.getGeneric(0).resolve(Object.class); + } else { + // 入参类型:(T) + if (processorExecutor == null) { + throw new IllegalArgumentException("节点决策器" + ClassUtils.getQualifiedMethodName(method) + "不能有非TargetContext入参,因为这个节点没有处理器"); + } + if (!parameterTypes[0].isAssignableFrom(processorExecutor.getReturnType())) { + throw new IllegalArgumentException("节点决策器" + ClassUtils.getQualifiedMethodName(method) + "的非TargetContext入参类型必须能被其处理器返回类型赋值"); + } + parametersType = FlowExecutor.NodeExecutor.NodeDeciderExecutor.ParametersType.ONLY_PROCESS_RESULT; + } + } else if (parameterTypes.length == 2) { + // 入参类型:(T, TargetContext) + if (processorExecutor == null) { + throw new IllegalArgumentException("节点决策器" + ClassUtils.getQualifiedMethodName(method) + "不能有非TargetContext入参,因为这个节点没有处理器"); + } + if (!parameterTypes[0].isAssignableFrom(processorExecutor.getReturnType())) { + throw new IllegalArgumentException("节点决策器" + ClassUtils.getQualifiedMethodName(method) + "的非TargetContext入参类型必须能被其处理器返回类型赋值"); + } + if (parameterTypes[1] != TargetContext.class) { + throw new IllegalArgumentException("节点决策器" + ClassUtils.getQualifiedMethodName(method) + "的第二个参数类型必须是TargetContext"); + } + parametersType = FlowExecutor.NodeExecutor.NodeDeciderExecutor.ParametersType.PROCESS_RESULT_AND_TARGET_CONTEXT; + // 解析目标对象类型 + ResolvableType resolvableType = ResolvableType.forMethodParameter(method, 1); + classOfTarget = resolvableType.getGeneric(0).resolve(Object.class); + } else { + throw new IllegalArgumentException("节点决策器" + ClassUtils.getQualifiedMethodName(method) + "的入参个数不能超过2个"); + } + } + // 校验返回类型 + if (method.isAnnotationPresent(EndNode.class)) { + if (method.getReturnType() != void.class) { + throw new IllegalArgumentException("结束节点的节点决策器" + ClassUtils.getQualifiedMethodName(method) + "的返回类型必须是void"); + } + } else { + if (method.getReturnType() != String.class) { + throw new IllegalArgumentException("节点决策器" + ClassUtils.getQualifiedMethodName(method) + "的返回类型必须是String"); + } + } + + return new FlowExecutor.NodeExecutor.NodeDeciderExecutor(method, parametersType, classOfTarget); + } + + // 解析目标对象映射方法 + private static FlowExecutor.TargetMappingExecutor parseTargetMapping(Method method) { + logger.debug("解析目标对象映射方法:{}", method); + // 校验方法类型 + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException("目标对象映射方法" + ClassUtils.getQualifiedMethodName(method) + "必须是public类型"); + } + // 校验入参 + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length != 1) { + throw new IllegalArgumentException("目标对象映射方法" + ClassUtils.getQualifiedMethodName(method) + "必须只能有一个入参"); + } + // 校验返回参数 + if (method.getReturnType() != String.class) { + throw new IllegalArgumentException("目标对象映射方法" + ClassUtils.getQualifiedMethodName(method) + "返回参数必须是String类型"); + } + + return new FlowExecutor.TargetMappingExecutor(method, parameterTypes[0]); + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/flow/FlowsHolder.java b/halo-flow/src/main/java/org/xujin/halo/flow/flow/FlowsHolder.java new file mode 100644 index 0000000..d3ad4b2 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/flow/FlowsHolder.java @@ -0,0 +1,66 @@ +package org.xujin.halo.flow.flow; + +import org.xujin.halo.event.bus.EventBusesHolder; +import org.xujin.halo.flow.annotation.flow.Flow; +import org.xujin.halo.flow.processor.ProcessorsHolder; +import org.xujin.halo.flow.transaction.FlowTxsHolder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * 流程持有器 + */ +@Component +public class FlowsHolder { + @Autowired + private ApplicationContext applicationContext; + @Autowired + private ProcessorsHolder processorsHolder; + @Autowired + private FlowTxsHolder flowTxsHolder; + @Autowired + private EventBusesHolder eventBusesHolder; + // 流程执行器Map(key:流程名称) + private Map flowExecutorMap = new HashMap<>(); + + // 初始化(查询spring容器中所有的@Flow流程并解析,spring自动执行) + @PostConstruct + public void init() { + String[] beanNames = applicationContext.getBeanNamesForAnnotation(Flow.class); + for (String beanName : beanNames) { + // 解析流程 + FlowExecutor flowExecutor = FlowParser.parseFlow(applicationContext.getBean(beanName), processorsHolder, flowTxsHolder, eventBusesHolder); + if (flowExecutorMap.containsKey(flowExecutor.getFlowName())) { + throw new RuntimeException("存在重名的流程" + flowExecutor.getFlowName()); + } + // 将执行器放入持有器中 + flowExecutorMap.put(flowExecutor.getFlowName(), flowExecutor); + } + } + + /** + * 获取所有流程名称 + */ + public Set getFlowNames() { + return flowExecutorMap.keySet(); + } + + /** + * 获取流程执行器 + * + * @param flow 流程名称 + * @throws IllegalArgumentException 如果不存在该流程执行器 + */ + public FlowExecutor getRequiredFlowExecutor(String flow) { + if (!flowExecutorMap.containsKey(flow)) { + throw new IllegalArgumentException("不存在流程" + flow); + } + return flowExecutorMap.get(flow); + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/listener/DefaultFlowListener.java b/halo-flow/src/main/java/org/xujin/halo/flow/listener/DefaultFlowListener.java new file mode 100644 index 0000000..e39d8a1 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/listener/DefaultFlowListener.java @@ -0,0 +1,37 @@ +package org.xujin.halo.flow.listener; + +import org.xujin.halo.event.EventPublisher; +import org.xujin.halo.event.annotation.Listen; +import org.xujin.halo.event.bus.EventBusesHolder; +import org.xujin.halo.event.publisher.DefaultEventPublisher; +import org.xujin.halo.flow.annotation.listener.FlowListener; +import org.xujin.halo.flow.event.FlowExceptionEvent; +import org.xujin.halo.flow.event.NodeDecidedEvent; +import org.springframework.context.annotation.DependsOn; + +/** + * 默认的流程监听器 + * (流程引擎初始化时会初始化本监听器,其作用是监听所有流程发生的事件,然后将事件转发给对应流程的特定流程监听器(@TheFlowListener)) + */ +@FlowListener +@DependsOn("EventBusesHolder") // 保证出现循环引用时不会出错 +public class DefaultFlowListener { + // 特定流程事件发布器 + private EventPublisher eventPublisher; + + public DefaultFlowListener(EventBusesHolder eventBusesHolder) { + eventPublisher = new DefaultEventPublisher(eventBusesHolder.getEventBus(TheFlowListenerType.class)); + } + + // 监听节点选择事件 + @Listen + public void listenNodeDecidedEvent(NodeDecidedEvent event) { + eventPublisher.publish(event); + } + + // 监听流程异常事件 + @Listen + public void listenFlowExceptionEvent(FlowExceptionEvent event) { + eventPublisher.publish(event); + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/listener/FlowListenerType.java b/halo-flow/src/main/java/org/xujin/halo/flow/listener/FlowListenerType.java new file mode 100644 index 0000000..6e9159c --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/listener/FlowListenerType.java @@ -0,0 +1,16 @@ +package org.xujin.halo.flow.listener; + +import org.xujin.halo.event.extension.EventTypeResolver; +import org.xujin.halo.event.extension.ListenerType; +import org.xujin.halo.event.extension.support.ClassEventTypeResolver; + +/** + * 流程监听器类型 + */ +public class FlowListenerType implements ListenerType { + + @Override + public EventTypeResolver getResolver() { + return ClassEventTypeResolver.INSTANCE; + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/listener/ListenFlowExceptionResolver.java b/halo-flow/src/main/java/org/xujin/halo/flow/listener/ListenFlowExceptionResolver.java new file mode 100644 index 0000000..5b93e6f --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/listener/ListenFlowExceptionResolver.java @@ -0,0 +1,46 @@ +package org.xujin.halo.flow.listener; + +import org.xujin.halo.event.extension.ListenResolver; +import org.xujin.halo.flow.annotation.listener.TheFlowListener; +import org.xujin.halo.flow.engine.TargetContext; +import org.xujin.halo.flow.event.FlowExceptionEvent; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Method; + +/** + * 监听注解@ListenFlowException的解决器 + */ +public class ListenFlowExceptionResolver implements ListenResolver { + // 监听的事件类型 + private TheFlowEventType eventType; + + @Override + public void init(Method listenMethod) { + TheFlowListener theFlowListenerAnnotation = AnnotatedElementUtils.findMergedAnnotation(listenMethod.getDeclaringClass(), TheFlowListener.class); + if (theFlowListenerAnnotation == null) { + throw new IllegalArgumentException("@ListenFlowException只能标注在特定流程监听器(@TheFlowListener)的方法上"); + } + // 校验入参 + Class[] parameterTypes = listenMethod.getParameterTypes(); + if (parameterTypes.length != 2) { + throw new RuntimeException("监听流程异常方法" + ClassUtils.getQualifiedMethodName(listenMethod) + "的入参必须是(Throwable, TargetContext)"); + } + if (parameterTypes[0] != Throwable.class || parameterTypes[1] != TargetContext.class) { + throw new RuntimeException("监听流程异常方法" + ClassUtils.getQualifiedMethodName(listenMethod) + "的入参必须是(Throwable, TargetContext)"); + } + eventType = new TheFlowEventType(theFlowListenerAnnotation.flow(), FlowExceptionEvent.class); + } + + @Override + public Object getEventType() { + return eventType; + } + + @Override + public Object[] resolveArgs(Object event) { + FlowExceptionEvent flowExceptionEvent = (FlowExceptionEvent) event; + return new Object[]{flowExceptionEvent.getThrowable(), flowExceptionEvent.getTargetContext()}; + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/listener/ListenNodeDecidedResolver.java b/halo-flow/src/main/java/org/xujin/halo/flow/listener/ListenNodeDecidedResolver.java new file mode 100644 index 0000000..aa052b3 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/listener/ListenNodeDecidedResolver.java @@ -0,0 +1,46 @@ +package org.xujin.halo.flow.listener; + +import org.xujin.halo.event.extension.ListenResolver; +import org.xujin.halo.flow.annotation.listener.TheFlowListener; +import org.xujin.halo.flow.engine.TargetContext; +import org.xujin.halo.flow.event.NodeDecidedEvent; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Method; + +/** + * 监听注解@ListenNodeDecided的解决器 + */ +public class ListenNodeDecidedResolver implements ListenResolver { + // 监听的事件类型 + private TheFlowEventType eventType; + + @Override + public void init(Method listenMethod) { + TheFlowListener theFlowListenerAnnotation = AnnotatedElementUtils.findMergedAnnotation(listenMethod.getDeclaringClass(), TheFlowListener.class); + if (theFlowListenerAnnotation == null) { + throw new IllegalArgumentException("@ListenNodeDecided只能标注在特定流程监听器(@TheFlowListener)的方法上"); + } + // 校验入参 + Class[] parameterTypes = listenMethod.getParameterTypes(); + if (parameterTypes.length != 2) { + throw new RuntimeException("监听节点选择方法" + ClassUtils.getQualifiedMethodName(listenMethod) + "的入参必须是(String, TargetContext)"); + } + if (parameterTypes[0] != String.class || parameterTypes[1] != TargetContext.class) { + throw new RuntimeException("监听节点选择方法" + ClassUtils.getQualifiedMethodName(listenMethod) + "的入参必须是(String, TargetContext)"); + } + eventType = new TheFlowEventType(theFlowListenerAnnotation.flow(), NodeDecidedEvent.class); + } + + @Override + public Object getEventType() { + return eventType; + } + + @Override + public Object[] resolveArgs(Object event) { + NodeDecidedEvent nodeDecidedEvent = (NodeDecidedEvent) event; + return new Object[]{nodeDecidedEvent.getNode(), nodeDecidedEvent.getTargetContext()}; + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/listener/TheFlowEventType.java b/halo-flow/src/main/java/org/xujin/halo/flow/listener/TheFlowEventType.java new file mode 100644 index 0000000..6de4dfc --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/listener/TheFlowEventType.java @@ -0,0 +1,35 @@ + +package org.xujin.halo.flow.listener; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Objects; + +/** + * 特定流程事件类型 + */ +public class TheFlowEventType { + // 流程 + private String flow; + // 类型 + private Class eventClass; + + public TheFlowEventType(String flow, Class eventClass) { + this.flow = flow; + this.eventClass = eventClass; + } + + @Override + public int hashCode() { + return Objects.hash(flow, eventClass); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof TheFlowEventType)) { + return false; + } + TheFlowEventType other = (TheFlowEventType) obj; + return StringUtils.equals(flow, other.flow) && eventClass == other.eventClass; + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/listener/TheFlowListenerType.java b/halo-flow/src/main/java/org/xujin/halo/flow/listener/TheFlowListenerType.java new file mode 100644 index 0000000..0302068 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/listener/TheFlowListenerType.java @@ -0,0 +1,34 @@ +package org.xujin.halo.flow.listener; + +import org.xujin.halo.event.extension.EventTypeResolver; +import org.xujin.halo.event.extension.ListenerType; +import org.xujin.halo.flow.event.FlowExceptionEvent; +import org.xujin.halo.flow.event.NodeDecidedEvent; + +/** + * 特定流程监听器类型 + */ +public class TheFlowListenerType implements ListenerType { + + @Override + public EventTypeResolver getResolver() { + return TheFlowEventTypeResolver.INSTANCE; + } + + // 特定流程事件类型解决器 + private static class TheFlowEventTypeResolver implements EventTypeResolver { + // 实例 + private static final TheFlowEventTypeResolver INSTANCE = new TheFlowEventTypeResolver(); + + @Override + public Object resolve(Object event) { + if (event instanceof NodeDecidedEvent) { + return new TheFlowEventType(((NodeDecidedEvent) event).getFlow(), NodeDecidedEvent.class); + } + if (event instanceof FlowExceptionEvent) { + return new TheFlowEventType(((FlowExceptionEvent) event).getFlow(), FlowExceptionEvent.class); + } + throw new IllegalArgumentException("无法识别的流程事件:" + event); + } + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/processor/ProcessorExecutor.java b/halo-flow/src/main/java/org/xujin/halo/flow/processor/ProcessorExecutor.java new file mode 100644 index 0000000..cdf087a --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/processor/ProcessorExecutor.java @@ -0,0 +1,162 @@ + +package org.xujin.halo.flow.processor; + +import org.springframework.util.ClassUtils; +import org.xujin.halo.flow.annotation.processor.*; +import org.xujin.halo.flow.engine.TargetContext; +import org.xujin.halo.method.MethodExecutor; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * 处理器执行器 + */ +public class ProcessorExecutor { + /** + * 处理器方法注解 + */ + public static final Class[] PROCESSOR_METHOD_ANNOTATIONS = {ProcessorBefore.class, ProcessorExecute.class, ProcessorAfter.class, ProcessorEnd.class, ProcessorError.class}; + + // 处理器名称 + private String processorName; + // 处理器 + private Object processor; + // 处理器方法执行器Map(key:处理器方法注解的Class) + private Map methodExecutorMap = new HashMap<>(); + + public ProcessorExecutor(String processorName, Object processor) { + this.processorName = processorName; + this.processor = processor; + } + + /** + * 执行处理器 + * (顺序:@ProcessorBefore、@ProcessorExecute、@ProcessorAfter、@ProcessorEnd;如果执行@ProcessorBefore、@ProcessorExecute、@ProcessorAfter发生异常,则会在执行@ProcessorEnd之前执行@ProcessorError) + * + * @param targetContext 目标上下文 + * @return Execute类型方法返回的结果 + * @throws Throwable 执行过程中发生任何异常都后会往外抛 + */ + public Object execute(TargetContext targetContext) throws Throwable { + try { + executeMethod(ProcessorBefore.class, targetContext); + Object result = executeMethod(ProcessorExecute.class, targetContext); + executeMethod(ProcessorAfter.class, targetContext); + return result; + } catch (Throwable e) { + executeMethod(ProcessorError.class, targetContext); + throw e; + } finally { + executeMethod(ProcessorEnd.class, targetContext); + } + } + + // 执行处理器方法(对于不存在对应的方法,则忽略并返回null) + private Object executeMethod(Class clazz, TargetContext targetContext) throws Throwable { + ProcessorMethodExecutor methodExecutor = methodExecutorMap.get(clazz); + if (methodExecutor == null) { + return null; + } + return methodExecutor.execute(processor, targetContext); + } + + /** + * 设置处理器方法执行器 + * + * @param clazz 处理器方法注解 + * @param methodExecutor 方法执行器 + * @throws IllegalArgumentException 如果annotationClass不是处理器方法注解 + * @throws IllegalStateException 如果已存在该类型的处理器方法执行器 + */ + public void setMethodExecutor(Class clazz, ProcessorMethodExecutor methodExecutor) { + if (!Arrays.asList(PROCESSOR_METHOD_ANNOTATIONS).contains(clazz)) { + throw new IllegalArgumentException(ClassUtils.getShortName(clazz) + "不是处理器方法注解"); + } + if (methodExecutorMap.containsKey(clazz)) { + throw new IllegalStateException("处理器" + processorName + "存在多个@" + ClassUtils.getShortName(clazz) + "类型的方法"); + } + methodExecutorMap.put(clazz, methodExecutor); + } + + /** + * 获取返回类型 + */ + public Class getReturnType() { + return methodExecutorMap.get(ProcessorExecute.class).getReturnType(); + } + + /** + * 获取目标对象类型 + */ + public Class getClassOfTarget() { + return methodExecutorMap.get(ProcessorExecute.class).getClassOfTarget(); + } + + /** + * 获取处理器名称 + */ + public String getProcessorName() { + return processorName; + } + + /** + * 获取处理器 + */ + public Object getProcessor() { + return processor; + } + + /** + * 校验处理器执行器是否有效 + * + * @throws IllegalStateException 校验不通过 + */ + public void validate() { + if (processorName == null || processor == null) { + throw new IllegalStateException("处理器" + processorName + "内部要素不全"); + } + if (!methodExecutorMap.containsKey(ProcessorExecute.class)) { + throw new IllegalStateException("处理器" + processorName + "不存在@ProcessorExecute类型的处理器方法"); + } + // 校验处理器内部目标对象类型是否统一 + for (ProcessorMethodExecutor methodExecutor : methodExecutorMap.values()) { + if (methodExecutor.getClassOfTarget() != getClassOfTarget()) { + throw new IllegalStateException("处理器" + processorName + "内目标对象类型不统一"); + } + } + } + + /** + * 处理器方法执行器 + */ + public static class ProcessorMethodExecutor extends MethodExecutor { + // 目标对象类型 + private Class classOfTarget; + + public ProcessorMethodExecutor(Method targetMethod, Class classOfTarget) { + super(targetMethod); + this.classOfTarget = classOfTarget; + } + + /** + * 执行处理器方法 + * + * @param processor 处理器 + * @param targetContext 目标上下文 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + public Object execute(Object processor, TargetContext targetContext) throws Throwable { + return execute(processor, new Object[]{targetContext}); + } + + /** + * 获取目标对象类型 + */ + public Class getClassOfTarget() { + return classOfTarget; + } + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/processor/ProcessorParser.java b/halo-flow/src/main/java/org/xujin/halo/flow/processor/ProcessorParser.java new file mode 100644 index 0000000..cb8fa10 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/processor/ProcessorParser.java @@ -0,0 +1,81 @@ +package org.xujin.halo.flow.processor; + +import org.apache.commons.lang3.StringUtils; +import org.xujin.halo.flow.annotation.processor.Processor; +import org.xujin.halo.flow.annotation.processor.ProcessorExecute; +import org.xujin.halo.flow.engine.TargetContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.ResolvableType; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * 处理器解析器 + */ +public class ProcessorParser { + // 日志记录器 + private static final Logger logger = LoggerFactory.getLogger(ProcessorParser.class); + + /** + * 解析处理器 + * + * @param processor 处理器 + * @return 处理器执行器 + */ + public static ProcessorExecutor parseProcessor(Object processor) { + // 获取目标class(应对AOP代理情况) + Class processorClass = AopUtils.getTargetClass(processor); + logger.debug("解析处理器:{}", ClassUtils.getQualifiedName(processorClass)); + // 获取处理器名称 + String processorName = processorClass.getAnnotation(Processor.class).name(); + if (StringUtils.isEmpty(processorName)) { + processorName = ClassUtils.getShortNameAsProperty(processorClass); + } + // 创建处理器执行器 + ProcessorExecutor processorExecutor = new ProcessorExecutor(processorName, processor); + for (Method method : processorClass.getDeclaredMethods()) { + for (Class clazz : ProcessorExecutor.PROCESSOR_METHOD_ANNOTATIONS) { + if (method.isAnnotationPresent(clazz)) { + // 设置处理器方法执行器 + processorExecutor.setMethodExecutor(clazz, parseProcessorMethod(clazz, method)); + break; + } + } + } + processorExecutor.validate(); + + return processorExecutor; + } + + /** + * 解析处理器方法 + */ + private static ProcessorExecutor.ProcessorMethodExecutor parseProcessorMethod(Class clazz, Method method) { + logger.debug("解析处理器方法:{}", method); + // 校验方法类型 + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException("处理器方法" + ClassUtils.getQualifiedMethodName(method) + "必须是public类型"); + } + // 校验入参 + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length != 1) { + throw new IllegalArgumentException("处理器方法" + ClassUtils.getQualifiedMethodName(method) + "入参必须是(TargetContext)"); + } + if (parameterTypes[0] != TargetContext.class) { + throw new IllegalArgumentException("处理器方法" + ClassUtils.getQualifiedMethodName(method) + "入参必须是(TargetContext)"); + } + // 校验返回类型 + if (clazz != ProcessorExecute.class && method.getReturnType() != void.class) { + throw new IllegalArgumentException("非@ProcessorExecute类型的处理器方法" + ClassUtils.getQualifiedMethodName(method) + "的返回类型必须是void"); + } + // 获取目标对象类型 + ResolvableType resolvableType = ResolvableType.forMethodParameter(method, 0); + Class classOfTarget = resolvableType.getGeneric(0).resolve(Object.class); + + return new ProcessorExecutor.ProcessorMethodExecutor(method, classOfTarget); + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/processor/ProcessorsHolder.java b/halo-flow/src/main/java/org/xujin/halo/flow/processor/ProcessorsHolder.java new file mode 100644 index 0000000..c742b93 --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/processor/ProcessorsHolder.java @@ -0,0 +1,57 @@ +package org.xujin.halo.flow.processor; + +import org.xujin.halo.flow.annotation.processor.Processor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * 处理器持有器 + */ +@Component +public class ProcessorsHolder { + @Autowired + private ApplicationContext applicationContext; + // 处理器执行器Map(key:处理器的名称) + private Map processorExecutorMap = new HashMap<>(); + + // 初始化(查询spring容器中所有的@Processor处理器并解析,spring自动执行) + @PostConstruct + public void init() { + String[] beanNames = applicationContext.getBeanNamesForAnnotation(Processor.class); + for (String beanName : beanNames) { + // 解析处理器 + ProcessorExecutor processorExecutor = ProcessorParser.parseProcessor(applicationContext.getBean(beanName)); + if (processorExecutorMap.containsKey(processorExecutor.getProcessorName())) { + throw new RuntimeException("存在重名的处理器:" + processorExecutor.getProcessorName()); + } + // 将执行器放入持有器中 + processorExecutorMap.put(processorExecutor.getProcessorName(), processorExecutor); + } + } + + /** + * 获取所有处理器名称 + */ + public Set getProcessorNames() { + return processorExecutorMap.keySet(); + } + + /** + * 获取处理器执行器 + * + * @param processor 处理器名称 + * @throws IllegalArgumentException 如果不存在该处理器执行器 + */ + public ProcessorExecutor getRequiredProcessorExecutor(String processor) { + if (!processorExecutorMap.containsKey(processor)) { + throw new IllegalArgumentException("不存在处理器:" + processor); + } + return processorExecutorMap.get(processor); + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/transaction/FlowTxExecutor.java b/halo-flow/src/main/java/org/xujin/halo/flow/transaction/FlowTxExecutor.java new file mode 100644 index 0000000..d6b69fb --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/transaction/FlowTxExecutor.java @@ -0,0 +1,171 @@ +package org.xujin.halo.flow.transaction; + +import org.xujin.halo.flow.annotation.transaction.InsertTarget; +import org.xujin.halo.flow.annotation.transaction.LockTarget; +import org.xujin.halo.flow.engine.TargetContext; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.util.ClassUtils; +import org.xujin.halo.method.MethodExecutor; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * 流程事务执行器 + */ +public class FlowTxExecutor extends TxExecutor { + /** + * 流程事务操作注解 + */ + public static final Class[] FLOW_TX_OPERATE_ANNOTATIONS = {LockTarget.class, InsertTarget.class}; + + // 对应的流程名称 + private String flow; + // 流程事务 + private Object flowTx; + // 操作执行器Map(key:流程事务操作注解的Class) + private Map operateExecutorMap = new HashMap<>(); + + public FlowTxExecutor(String flow, Object flowTx, PlatformTransactionManager transactionManager) { + super(transactionManager); + this.flow = flow; + this.flowTx = flowTx; + } + + /** + * 锁住目标对象 + * + * @param targetContext 目标上下文 + * @throws IllegalStateException 如果不存在@LockTarget类型操作 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + public void lockTarget(TargetContext targetContext) throws Throwable { + // 执行@LockTarget类型操作执行器 + FlowTxOperateExecutor operateExecutor = operateExecutorMap.get(LockTarget.class); + if (operateExecutor == null) { + throw new IllegalStateException("流程事务" + ClassUtils.getShortName(flowTx.getClass()) + "不存在@LockTarget类型操作"); + } + targetContext.refreshTarget(operateExecutor.execute(flowTx, targetContext)); + } + + /** + * 创建新事务插入目标对象并提交事务 + * + * @param targetContext 目标上下文 + * @throws IllegalStateException 如果不存在@InsertTarget类型操作 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + public void insertTarget(TargetContext targetContext) throws Throwable { + // 创建事务 + createTx(); + try { + // 执行@InsertTarget类型操作执行器 + FlowTxOperateExecutor operateExecutor = operateExecutorMap.get(InsertTarget.class); + if (operateExecutor == null) { + throw new IllegalStateException("流程事务" + ClassUtils.getShortName(flowTx.getClass()) + "不存在@InsertTarget类型操作"); + } + targetContext.refreshTarget(operateExecutor.execute(flowTx, targetContext)); + // 提交事务 + commitTx(); + } catch (Throwable e) { + // 回滚事务 + rollbackTx(); + throw e; + } + } + + /** + * 设置流程事务操作执行器 + * + * @param clazz 流程事务操作注解的Class + * @param operateExecutor 流程事务操作执行器 + * @throws IllegalArgumentException 如果入参clazz不是流程事务操作注解 + * @throws IllegalStateException 如果已存在该类型的操作处理器 + */ + public void setOperateExecutor(Class clazz, FlowTxOperateExecutor operateExecutor) { + if (!Arrays.asList(FLOW_TX_OPERATE_ANNOTATIONS).contains(clazz)) { + throw new IllegalArgumentException(ClassUtils.getShortName(clazz) + "不是流程事务操作注解"); + } + if (operateExecutorMap.containsKey(clazz)) { + throw new IllegalStateException("流程事务" + ClassUtils.getShortName(flowTx.getClass()) + "存在多个@" + ClassUtils.getShortName(clazz) + "类型的操作"); + } + operateExecutorMap.put(clazz, operateExecutor); + } + + /** + * 获取对应流程名称 + */ + public String getFlow() { + return flow; + } + + /** + * 获取流程事务 + */ + public Object getFlowTx() { + return flowTx; + } + + /** + * 获取目标对象类型 + */ + public Class getClassOfTarget() { + return operateExecutorMap.get(LockTarget.class).getClassOfTarget(); + } + + /** + * 校验流程事务执行器有效性 + * + * @throws IllegalStateException 如果校验不通过 + */ + @Override + public void validate() { + super.validate(); + if (flow == null || flowTx == null) { + throw new IllegalStateException("流程事务" + ClassUtils.getShortName(flowTx.getClass()) + "内部要素不全"); + } + if (!operateExecutorMap.containsKey(LockTarget.class)) { + throw new IllegalStateException("流程事务" + ClassUtils.getShortName(flowTx.getClass()) + "不存在@LockTarget类型操作"); + } + // 校验流程事务内目标对象类型是否统一 + for (FlowTxOperateExecutor operateExecutor : operateExecutorMap.values()) { + if (operateExecutor.getClassOfTarget() != getClassOfTarget()) { + throw new IllegalStateException("流程事务" + ClassUtils.getShortName(flowTx.getClass()) + "内目标对象类型不统一"); + } + } + } + + /** + * 流程事务操作执行器 + */ + public static class FlowTxOperateExecutor extends MethodExecutor { + // 目标对象类型 + private Class classOfTarget; + + public FlowTxOperateExecutor(Method targetMethod, Class classOfTarget) { + super(targetMethod); + this.classOfTarget = classOfTarget; + } + + /** + * 执行流程事务操作 + * + * @param flowTx 流程事务 + * @param targetContext 目标上下文 + * @return 该操作后的目标对象 + * @throws Throwable 执行过程中发生任何异常都会往外抛 + */ + public Object execute(Object flowTx, TargetContext targetContext) throws Throwable { + return execute(flowTx, new Object[]{targetContext}); + } + + /** + * 获取目标对象类型 + */ + public Class getClassOfTarget() { + return classOfTarget; + } + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/transaction/FlowTxParser.java b/halo-flow/src/main/java/org/xujin/halo/flow/transaction/FlowTxParser.java new file mode 100644 index 0000000..68d0f4f --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/transaction/FlowTxParser.java @@ -0,0 +1,75 @@ +package org.xujin.halo.flow.transaction; + +import org.xujin.halo.flow.annotation.transaction.FlowTx; +import org.xujin.halo.flow.engine.TargetContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.ResolvableType; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * 流程事务解析器 + */ +public class FlowTxParser { + // 日志记录器 + private static final Logger logger = LoggerFactory.getLogger(FlowTxParser.class); + + /** + * 解析流程事务 + * + * @param flowTx 流程事务 + * @param transactionManager 事务管理器 + * @return 流程事务执行器 + */ + public static FlowTxExecutor parseFlowTx(Object flowTx, PlatformTransactionManager transactionManager) { + // 获取目标class(应对AOP代理情况) + Class flowTxClass = AopUtils.getTargetClass(flowTx); + logger.debug("解析流程事务:{}", ClassUtils.getQualifiedName(flowTxClass)); + FlowTx flowTxAnnotation = flowTxClass.getAnnotation(FlowTx.class); + // 创建流程事务执行器 + FlowTxExecutor flowTxExecutor = new FlowTxExecutor(flowTxAnnotation.flow(), flowTx, transactionManager); + for (Method method : flowTxClass.getDeclaredMethods()) { + for (Class clazz : FlowTxExecutor.FLOW_TX_OPERATE_ANNOTATIONS) { + if (method.isAnnotationPresent(clazz)) { + // 设置流程事务操作执行器 + flowTxExecutor.setOperateExecutor(clazz, parseFlowTxOperate(method)); + break; + } + } + } + flowTxExecutor.validate(); + + return flowTxExecutor; + } + + // 解析流程事务操作 + private static FlowTxExecutor.FlowTxOperateExecutor parseFlowTxOperate(Method method) { + logger.debug("解析流程事务方法:{}", method); + // 校验方法类型 + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException("流程事务方法" + ClassUtils.getQualifiedMethodName(method) + "必须是public类型"); + } + // 校验入参 + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length != 1) { + throw new IllegalArgumentException("流程事务方法" + ClassUtils.getQualifiedMethodName(method) + "的入参必须是(TargetContext)"); + } + if (parameterTypes[0] != TargetContext.class) { + throw new IllegalArgumentException("流程事务方法" + ClassUtils.getQualifiedMethodName(method) + "的入参必须是(TargetContext)"); + } + // 获取目标对象类型 + ResolvableType resolvableType = ResolvableType.forMethodParameter(method, 0); + Class classOfTarget = resolvableType.getGeneric(0).resolve(Object.class); + // 校验返回参数 + if (method.getReturnType() != classOfTarget) { + throw new IllegalArgumentException("流程事务方法" + ClassUtils.getQualifiedMethodName(method) + "的返回类型必须是目标对象类型"); + } + + return new FlowTxExecutor.FlowTxOperateExecutor(method, classOfTarget); + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/transaction/FlowTxsHolder.java b/halo-flow/src/main/java/org/xujin/halo/flow/transaction/FlowTxsHolder.java new file mode 100644 index 0000000..c9035ad --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/transaction/FlowTxsHolder.java @@ -0,0 +1,63 @@ +package org.xujin.halo.flow.transaction; + +import org.xujin.halo.flow.annotation.transaction.FlowTx; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * 流程事务持有器 + */ +@Component +public class FlowTxsHolder { + @Autowired + private ApplicationContext applicationContext; + @Autowired(required = false) + private PlatformTransactionManager transactionManager; + // 流程事务执行器Map(key:流程事务对应的流程名称) + private Map flowTxExecutorMap = new HashMap<>(); + + // 初始化(查询spring容器中所有的@FlowTx流程事务并解析,spring自动执行) + @PostConstruct + public void init() { + String[] beanNames = applicationContext.getBeanNamesForAnnotation(FlowTx.class); + if (beanNames.length > 0 && transactionManager == null) { + throw new RuntimeException("存在流程事务但是不存在事务管理器(PlatformTransactionManager),请检查是否有配置spring事务管理器"); + } + for (String beanName : beanNames) { + // 解析流程事务 + FlowTxExecutor flowTxExecutor = FlowTxParser.parseFlowTx(applicationContext.getBean(beanName), transactionManager); + if (flowTxExecutorMap.containsKey(flowTxExecutor.getFlow())) { + throw new RuntimeException("流程" + flowTxExecutor.getFlow() + "存在多个流程事务"); + } + // 将执行器放入持有器中 + flowTxExecutorMap.put(flowTxExecutor.getFlow(), flowTxExecutor); + } + } + + /** + * 获取所有流程事务对应的流程名称 + */ + public Set getFlowNames() { + return flowTxExecutorMap.keySet(); + } + + /** + * 获取流程事务执行器 + * + * @param flow 流程名称 + * @throws IllegalArgumentException 如果不存在该流程事务处理器 + */ + public FlowTxExecutor getRequiredFlowTxExecutor(String flow) { + if (!flowTxExecutorMap.containsKey(flow)) { + throw new IllegalArgumentException("不存在流程" + flow + "的流程事务"); + } + return flowTxExecutorMap.get(flow); + } +} diff --git a/halo-flow/src/main/java/org/xujin/halo/flow/transaction/TxExecutor.java b/halo-flow/src/main/java/org/xujin/halo/flow/transaction/TxExecutor.java new file mode 100644 index 0000000..f26e20d --- /dev/null +++ b/halo-flow/src/main/java/org/xujin/halo/flow/transaction/TxExecutor.java @@ -0,0 +1,72 @@ +package org.xujin.halo.flow.transaction; + +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +/** + * 事务执行器 + */ +public class TxExecutor { + // 事务定义(传播行为是REQUIRES_NEW,即每次都开启一个新事务) + private static final TransactionDefinition TX_DEFINITION = new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + // 事务管理器 + private PlatformTransactionManager transactionManager; + // 事务持有器 + private ThreadLocal txStatusHolder = new ThreadLocal<>(); + + public TxExecutor(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + /** + * 创建事务 + * + * @throws IllegalStateException 如果已存在事务 + */ + public void createTx() { + if (txStatusHolder.get() != null) { + throw new IllegalStateException("本线程事务已存在,不能同时创建多个事务"); + } + txStatusHolder.set(transactionManager.getTransaction(TX_DEFINITION)); + } + + /** + * 提交事务 + * + * @throws IllegalStateException 如果不存在事务 + */ + public void commitTx() { + if (txStatusHolder.get() == null) { + throw new IllegalStateException("事务不存在,无法提交事务"); + } + transactionManager.commit(txStatusHolder.get()); + txStatusHolder.remove(); + } + + /** + * 回滚事务 + * + * @throws IllegalStateException 如果不存在事务 + */ + public void rollbackTx() { + if (txStatusHolder.get() == null) { + throw new IllegalStateException("事务不存在,无法回滚事务"); + } + transactionManager.rollback(txStatusHolder.get()); + txStatusHolder.remove(); + } + + /** + * 校验事务执行器是否有效 + * + * @throws IllegalStateException 如果校验不通过 + */ + public void validate() { + if (transactionManager == null) { + throw new IllegalStateException("事务执行器内部要素不全"); + } + } +} diff --git a/halo-flow/src/main/resources/META-INF/spring.factories b/halo-flow/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..08fe152 --- /dev/null +++ b/halo-flow/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.xujin.halo.flow.boot.FlowEngineAutoConfiguration \ No newline at end of file diff --git a/halo-test/pom.xml b/halo-test/pom.xml new file mode 100644 index 0000000..c4ef921 --- /dev/null +++ b/halo-test/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + org.xujin.halo + halo + 1.0.4 + ../pom.xml + + halo-test + 1.0.4 + halo-test + + + junit + junit + compile + + + org.springframework + spring-core + + + org.springframework + spring-context + + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + + jar-no-fork + + + + + + + diff --git a/halo-test/src/main/java/org/xujin/halo/test/TestExecutor.java b/halo-test/src/main/java/org/xujin/halo/test/TestExecutor.java new file mode 100644 index 0000000..f2c03cd --- /dev/null +++ b/halo-test/src/main/java/org/xujin/halo/test/TestExecutor.java @@ -0,0 +1,181 @@ +package org.xujin.halo.test; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; + +/** + * Run test here + * @author xujin 2017年10月27日 下午3:14:21 + */ +public class TestExecutor { + + private String className; + private String methodName; + + private Map testInstanceCache = new HashMap(); + + private ApplicationContext context; + + public TestExecutor(ApplicationContext context){ + this.context = context; + } + + public void testClass() throws Exception { + Class testClz = Class.forName(className); + Object testInstance = getTestInstance(testClz); + runClassTest(testClz, testInstance); + } + + public void testMethod() throws Exception { + Class testClz = Class.forName(className); + Object testInstance = getTestInstance(testClz); + runMethodTest(testClz, testInstance); + } + + private void runMethodTest(Class testClz, Object testInstance) throws Exception{ + Method[] allMethods = testClz.getDeclaredMethods(); + Method beforeMethod = null; + Method afterMethod = null; + for (Method method : allMethods){ + Annotation[] annotations = method.getAnnotations(); + for(Annotation annotation : annotations){ + if(annotation instanceof Before){ + beforeMethod = method; + break; + } + if(annotation instanceof After){ + afterMethod = method; + break; + } + } + } + //invoke before method + invokeMethod(testInstance, beforeMethod); + //invoke test method + invokeMethod(testInstance, testClz.getMethod(methodName)); + //invoke after method + invokeMethod(testInstance, afterMethod); + } + + private Object getTestInstance(Class testClz) throws Exception{ + if(testInstanceCache.get(className) != null){ + return testInstanceCache.get(className); + } + Object testInstance = testClz.newInstance(); + injectWiredBean(testClz, testInstance); + return testInstance; + } + + private void runClassTest(Class testClz, Object testInstance)throws Exception{ + Method[] allMethods = testClz.getDeclaredMethods(); + Method beforeMethod = null; + Method afterMethod = null; + List testMethods = new ArrayList(); + for (Method method : allMethods){ + Annotation[] annotations = method.getAnnotations(); + for(Annotation annotation : annotations){ + if(annotation instanceof Before){ + beforeMethod = method; + break; + } + if(annotation instanceof After){ + afterMethod = method; + break; + } + if(annotation instanceof Test || method.getName().startsWith("test")){ + testMethods.add(method); + break; + } + } + } + //invoke before method + invokeMethod(testInstance, beforeMethod); + //invoke test methods + for(Method testMethod: testMethods){ + invokeMethod(testInstance, testMethod); + } + //invoke after method + invokeMethod(testInstance, afterMethod); + } + + private static void invokeMethod(Object obj, Method method) throws Exception{ + if (method == null){ + return; + } + + method.invoke(obj); + } + + private void injectWiredBean(Class testClz, Object testInstance) { + Field[] fields = testClz.getDeclaredFields(); + if(fields == null){ + return; + } + for(Field field : fields) { + String beanName = field.getName(); + Annotation autowiredAnn = field.getDeclaredAnnotation(Autowired.class); + if (autowiredAnn == null) { + //System.out.println("Field "+beanName+" is not autowired, just ignore it"); + return; + } + try { + field.setAccessible(true); + field.set(testInstance, context.getBean(beanName)); + } catch (BeansException | IllegalArgumentException | IllegalAccessException e) { + //try to use type to get bean + try { + field.set(testInstance, context.getBean(field.getType())); + } catch (Exception innerE) { + System.err.println("oops!!! "+beanName + " can not be injected to "+ className); + System.err.println(innerE.getMessage()); + } + } + } + } + + + /** + * @return the className + */ + public String getClassName() { + return className; + } + + + /** + * @param className the className to set + */ + public void setClassName(String className) { + this.className = className; + } + + + /** + * @return the methodName + */ + public String getMethodName() { + return methodName; + } + + + /** + * @param methodName the methodName to set + */ + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + +} \ No newline at end of file diff --git a/halo-test/src/main/java/org/xujin/halo/test/TestsContainer.java b/halo-test/src/main/java/org/xujin/halo/test/TestsContainer.java new file mode 100644 index 0000000..ccb7984 --- /dev/null +++ b/halo-test/src/main/java/org/xujin/halo/test/TestsContainer.java @@ -0,0 +1,150 @@ +package org.xujin.halo.test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * 这是一个轻量级的TDD测试工具,可以敏捷的在开发过程中,运行测试,功能如下: + * 1.测试单个方法,请在控制台输入方法全称 + * 例如:org.xujin.halo.sales.service.test.CustomerServiceTest.testCheckConflict() + * 2.测试整个测试类,请在控制台输入类全称 + * 例如:org.xujin.halo.sales.service.test.CustomerServiceTest + * 3.重复上一次测试,只需在控制台输入字母 - ‘r’ + * + * @author xujin + * + */ +@Component +public class TestsContainer implements ApplicationContextAware{ + + private static final String WELCOME_INPUT = "$Welcome$"; + + private static final String REPEAT_INPUT = "r"; + + private static ApplicationContext context; + + private static TestExecutor testExecutor; + + private static boolean isInputValid = true; + + /** + * TestsContainer is optional to be in Spring Container + * @param context ApplicationContext to be provided + */ + public static void start(ApplicationContext context) { + TestsContainer.context = context; + start(); + } + + /** + * TestsContainer must be within Spring Container + */ + public static void start( ){ + + testExecutor = new TestExecutor(context); + + BufferedReader bufferRead = new BufferedReader(new InputStreamReader( + System.in)); + String input = WELCOME_INPUT; + String lastInput = input; + while (!(input).equalsIgnoreCase("quit")) { + try { + isInputValid = true; + if (REPEAT_INPUT.equalsIgnoreCase(input)){ + input = lastInput; + } + //execute + execute(input); + + } catch (Throwable e) { + e.printStackTrace(); + } + finally{ + if (!REPEAT_INPUT.equalsIgnoreCase(input) && isInputValid){ + lastInput = input; + } + try { + input = bufferRead.readLine(); + } catch (IOException e) { + e.printStackTrace(); + return; + } + } + } + } + + private static void execute(String input) throws Exception{ + if (input == null || input.trim().length()==0){ + isInputValid = false; + System.err.println("Your input is empty, please input a valid qualified name"); + return; + } + if (WELCOME_INPUT.equals(input)){ + System.out.println("************** 欢迎使用轻量级TDD测试工具 ***************************"); + System.out.println("**** 1.测试单个方法,请在控制台输入方法全称"); + System.out.println("**** 例如:org.xujin.halo.sales.service.test.CustomerServiceTest.testCheckConflict()"); + System.out.println("**** 2.测试整个测试类,请在控制台输入类全称"); + System.out.println("**** 例如:org.xujin.halo.sales.service.test.CustomerServiceTest"); + System.out.println("**** 3.重复上一次测试,只需在控制台输入字母 - ‘r’"); + System.out.println("***********************************************************************************"); + return; + } + if (input.indexOf(".") == -1){ + isInputValid = false; + System.err.println("Your input is not a valid qualified name"); + return; + } + boolean isMethod = false; + if (isEclipseMethod(input) || isIdeaMethod(input)){ + isMethod = true; + } + System.out.println("===Run "+(isMethod?"Method":"Class")+" start==== "+input); + if(isMethod){ + String className = null; + String methodName = null; + if (isEclipseMethod(input)) { + methodName = input.substring(input.lastIndexOf(".")+1, input.indexOf("(")); + className = input.substring(0, input.lastIndexOf(".")); + } + if (isIdeaMethod(input)) { + methodName = input.substring(input.lastIndexOf("#")+1, input.length()); + className = input.substring(0, input.lastIndexOf("#")); + } + if(methodName == null || className == null) { + System.err.println("Your input " + input + " is not valid"); + return; + } + testExecutor.setClassName(className); + testExecutor.setMethodName(methodName); + testExecutor.testMethod(); + } + else { + testExecutor.setClassName(input); + testExecutor.testClass(); + } + System.out.println("===Run "+(isMethod?"Method":"Class")+" end====\n"); + } + + /** + * @param input + * @return + */ + private static boolean isEclipseMethod(String input) { + return input.indexOf("(") > 0 ; + } + + private static boolean isIdeaMethod(String input) { + return input.indexOf("#") > 0 ;//to accommodate idea + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + context = applicationContext; + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9a0bb50 --- /dev/null +++ b/pom.xml @@ -0,0 +1,205 @@ + + + + + + org.springframework.boot + spring-boot-starter-parent + 1.5.12.RELEASE + + + 4.0.0 + org.xujin.halo + halo + pom + 1.0.4 + halo + + + 1.8 + 1.8 + 1.8 + UTF-8 + UTF-8 + 1.10.19 + 1.1.1 + 4.3.5.RELEASE + 1.4.3.RELEASE + 4.3.2.RELEASE + 4.12 + 1.2.3.5-struct + 1.7.22 + 1.0.4 + 1.0 + 1.8.3 + + + + halo-test + halo-common + halo-core + halo-flow + halo-event + halo-collection + halo-base + + + + + + + org.xujin.halo + halo-test + ${halo.framework.version} + test + + + org.xujin.halo + halo-common + ${halo.framework.version} + + + + org.xujin.halo + halo-base + ${halo.framework.version} + + + + org.xujin.halo + halo-core + ${halo.framework.version} + + + + + org.mybatis + mybatis + 3.4.2 + + + org.mybatis + mybatis-spring + 1.3.1 + + + + + org.springframework + spring-core + ${spring-version} + + + org.springframework + spring-beans + ${spring-version} + + + org.springframework + spring-aop + ${spring-version} + + + org.springframework + spring-context + ${spring-version} + + + org.springframework + spring-context-support + ${spring-version} + + + + + org.slf4j + slf4j-api + 1.7.9 + + + ch.qos.logback + logback-core + ${logback.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + org.mockito + mockito-all + ${mockito-all.version} + test + + + junit + junit + ${junit.version} + test + + + org.springframework + spring-test + ${spring-test.version} + test + + + + + org.apache.commons + commons-lang3 + 3.7 + + + + org.projectlombok + lombok + 1.12.6 + + + org.reflections + reflections + 0.9.10 + + + + + javax.validation + validation-api + 2.0.0.Final + + + org.hibernate.validator + hibernate-validator + 6.0.7.Final + + + javax.el + javax.el-api + 3.0.0 + + + org.glassfish.web + javax.el + 2.2.6 + + + + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + + +