能力说明:
精通JVM运行机制,包括类生命、内存模型、垃圾回收及JVM常见参数;能够熟练使用Runnable接口创建线程和使用ExecutorService并发执行任务、识别潜在的死锁线程问题;能够使用Synchronized关键字和atomic包控制线程的执行顺序,使用并行Fork/Join框架;能过开发使用原始版本函数式接口的代码。
能力说明:
掌握Java开发环境下所需的MySQL高级技巧,包括索引策略、innodb和myisam存储引擎,熟悉MySQL锁机制,能熟练配置MySQL主从复制,熟练掌握日常SQL诊断和性能分析工具和策略。可对云数据库进行备份恢复与监控、安全策略的设置,并可对云数据库进行性能优化。掌握主要NOSQL数据库的应用技术。
暂时未有相关云产品技术能力~
Janino官网Janino是一个轻量级的Java编译器。作为Library,它可以直接在Java程序中调用,动态编译java代码并加载。编译时可以直接引用JVM中已经加载的类.支持绝大部分java语法,官方网站上有详细各个版本支持的语法.依据官方的说法简单表达式编译甚至比jdk更快。官网上的例子及介绍十分详细,有兴趣的同学可以直接查看官网。增加pom依赖<dependency> <groupId>org.codehaus.janino</groupId> <artifactId>janino</artifactId> <version>3.0.7</version> </dependency>基础用法简单表达式执行示例import org.codehaus.janino.*; public class JaninoDemo { public static void main(String[] args) throws InvocationTargetException, CompileException { ExpressionEvaluator ee = new ExpressionEvaluator(); ee.setParameters(new String[]{"param"}, new Class[]{Param.class}); ee.setExpressionType(String.class); ee.cook("param.getA() + param.getB()"); Param param = new Param(); param.setA("a"); param.setB("B"); String resultStr = (String) ee.evaluate(new Param[]{param}); System.out.println(resultStr); } } //参数实体 public class Param{ private String a; private String b; public String getA() { return a; } public void setA(String a) { this.a = a; } public String getB() { return b; } public void setB(String b) { this.b = b; } }输出结果Connected to the target VM, address: '127.0.0.1:3797', transport: 'socket' aB关键代码说明ee.setParameters:设置入参类型ee.setExpressionType:设置返回值类型ee.cook:设置需处理的表达式,支持多种格式String/File/InputStream……ee.evaluate:执行表达式并获取结果参数所对应的JavaBean需具备get/set方法,不支持lombok相关注解。代码段执行示例: public class JaninoDemo { public static void main(String[] args) throws InvocationTargetException, CompileException { scriptEvaluator(); } public static void expressionEvaluator() { try { ExpressionEvaluator ee = new ExpressionEvaluator(); ee.setParameters(new String[]{"a", "b"}, new Class[]{int.class, int.class}); ee.setExpressionType(int.class); ee.cook("a + b"); int result = (Integer) ee.evaluate(new Integer[]{8, 9}); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } } public static void scriptEvaluator() throws CompileException, InvocationTargetException { ScriptEvaluator se = new ScriptEvaluator(); se.cook("import static com.meijm.toolbox.janino.JaninoDemo.*;" + "expressionEvaluator();"); se.setDebuggingInformation(true, true, true); se.evaluate(null); } }注意事项cook中的代码块支持常见java语法。se.setDebuggingInformation:设置调试开关,打开后脚本对应的方法支持调试脚本中调用的静态方法expressionEvaluator() 不能抛出异常,需自己处理异常信息。参考资料https://janino-compiler.github.io/janino/https://github.com/janino-compiler/janino/tree/master/janino/src/test/java/org/codehaus/janino/tests
Activiti监听任务的几种方式全局监听监听类@Component public class TaskCompletedEventListener implements ActivitiEventListener { @Override public void onEvent(ActivitiEvent event) { if (!event.getType().equals(TASK_COMPLETED)) { return; } //监听处理 } @Override public boolean isFailOnException() { return true; } }onEvent:事件处理代码isFailOnException:出现异常时当前操作是否失败TASK_COMPLETED:为org.activiti.engine.delegate.event.ActivitiEventType枚举值。通过查看引用可查看抛出事件携带的参数,便于从event中获取携的参数。源码ActivitiEventSupport.dispatchEvent中使用isFailOnException返回值来判断是否继续抛出异常Spring 配置类@Configuration public class ActivitiConfig implements ProcessEngineConfigurationConfigurer { @Lazy @Autowired private List<ActivitiEventListener> activitiEventListeners; @Override public void configure(SpringProcessEngineConfiguration processEngineConfiguration) { processEngineConfiguration.setEventListeners(activitiEventListeners); } }ProcessEngineConfigurationConfigurer:为activiti-spring-boot提供的activiti配置入口,继承重写configure方法即可设置activiti配置。activitiEventListeners注入说明:Spring集合注入会将Spring中托管的子类都注入此集合TaskListener监听通过bpmn文件配置 <userTask id="任务id" name="任务名称" activiti:candidateUsers="候选用户"> <extensionElements> <activiti:taskListener event="create" delegateExpression="${taskStartListener}"> <activiti:field name="url" stringValue="333"/> </activiti:taskListener> </extensionElements> </userTask>delegateExpression:代理表达式,支持读取Spring托管的beanevent : 监听的节点assignment/设置代理人,complete/任务完成,create/创建任务activiti:field 监听类扩展参数,对应类中使用Expression读取配置文件中指定内容,除了字符串还可以指定为动态表达式例如:expression="Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}",表达式中可访问的内容包含流程变量及spring中定义的bean@Data @Service("taskStartListener") public class TaskStartListener implements TaskListener { protected Expression textExp; public void notify(DelegateTask delegateTask) { String text = textExp.getExpressionText(); //任务开始监听 } }ExecutionListener监听通过bpmn文件配置 <userTask id="任务id" name="任务名称" activiti:candidateUsers="候选用户"> <extensionElements> <activiti:executionListener event="start" delegateExpression="${executionStartListener}"> <activiti:field name="start" stringValue="start"/> </activiti:executionListener> </extensionElements> </userTask>executionListener: 执行监听,可用于监听流程,环节,连接线,比任务监听范围更广但没有assignment这种针对任务的监听delegateExpression:代理表达式,支持读取Spring托管的beanevent : 监听的节点start/节点开始,end/节点结束,take/监听连接线activiti:field 监听类扩展参数@Data @Service("executionStartListener") public class ExecutionStartListener implements ExecutionListener { protected Expression textExp; @Override public void notify(DelegateExecution execution) { String text = textExp.getExpressionText(); //执行开始监听 } }参考资料https://www.activiti.org/userguide/
JOLJOL 是OpenJdk发布的用查看对象在JVM中信息,全称是Java Object Layout。可以通过命令行使用。下面例子中仅使用到了ClassLayout,有兴趣可以看看官网介绍。pom依赖,jol 0.16版本没有使用字节码展示对象头而是直接展示了锁信息,与jstack日志一致,如果想查看对象头变化请使用 0.10版 <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.16</version> </dependency>synchronizationsynchronization是同步关键字,下面的示例使用synchronization+jol查看锁信息。有兴趣可以直接运行看看效果,也可以直接拖到最后看看结论或者看看oracle官方解释。JAVA代码import lombok.extern.slf4j.Slf4j; import org.openjdk.jol.info.ClassLayout; @Slf4j public class SynchronizedDemo { public static void main(String[] args) { SynchronizedDemo demo = new SynchronizedDemo(); SynchronizedDemo demo1 = new SynchronizedDemo(); SynchronizedDemo demo2 = new SynchronizedDemo(); new Thread(() -> { demo.lockMethod(); }).start(); new Thread(() -> { demo.lockInstance(demo1); }).start(); new Thread(() -> { SynchronizedDemo.lockStaticMethod(); }).start(); log.info("打印demo头信息"); log.info(ClassLayout.parseInstance(demo).toPrintable()); log.info("打印demo1头信息"); log.info(ClassLayout.parseInstance(demo1).toPrintable()); log.info("打印demo2头信息"); log.info(ClassLayout.parseInstance(demo2).toPrintable()); log.info("打印Class头信息"); log.info(ClassLayout.parseInstance(SynchronizedDemo.class).toPrintable()); } public synchronized static void lockStaticMethod() { try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void lockMethod() { try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } public void lockInstance(SynchronizedDemo demo) { synchronized (demo) { try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } } }打印日志2022-06-15 15:28:48 INFO --- [main ] c.m.basis.concurrent.SynchronizedDemo : 打印demo头信息 2022-06-15 15:28:49 INFO --- [main ] c.m.basis.concurrent.SynchronizedDemo : com.meijm.basis.concurrent.SynchronizedDemo object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x000000002097f050 (thin lock: 0x000000002097f050) 8 4 (object header: class) 0xf800c105 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 2022-06-15 15:28:49 INFO --- [main ] c.m.basis.concurrent.SynchronizedDemo : 打印demo1头信息 2022-06-15 15:28:49 INFO --- [main ] c.m.basis.concurrent.SynchronizedDemo : com.meijm.basis.concurrent.SynchronizedDemo object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x0000000020a7ee10 (thin lock: 0x0000000020a7ee10) 8 4 (object header: class) 0xf800c105 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 2022-06-15 15:28:49 INFO --- [main ] c.m.basis.concurrent.SynchronizedDemo : 打印demo2头信息 2022-06-15 15:28:49 INFO --- [main ] c.m.basis.concurrent.SynchronizedDemo : com.meijm.basis.concurrent.SynchronizedDemo object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 4 (object header: class) 0xf800c105 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 2022-06-15 15:28:49 INFO --- [main ] c.m.basis.concurrent.SynchronizedDemo : 打印Class头信息 2022-06-15 15:28:49 INFO --- [main ] c.m.basis.concurrent.SynchronizedDemo : java.lang.Class object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x000000001ba5570a (fat lock: 0x000000001ba5570a) 8 4 (object header: class) 0xf80003df 12 4 java.lang.reflect.Constructor Class.cachedConstructor null 16 4 java.lang.Class Class.newInstanceCallerCache null 20 4 java.lang.String Class.name (object) 24 4 (alignment/padding gap) 28 4 java.lang.ref.SoftReference Class.reflectionData (object) 32 4 sun.reflect.generics.repository.ClassRepository Class.genericInfo null 36 4 java.lang.Object[] Class.enumConstants null 40 4 java.util.Map Class.enumConstantDirectory null 44 4 java.lang.Class.AnnotationData Class.annotationData (object) 48 4 sun.reflect.annotation.AnnotationType Class.annotationType null 52 4 java.lang.ClassValue.ClassValueMap Class.classValueMap null 56 32 (alignment/padding gap) 88 4 int Class.classRedefinedCount 0 92 4 (object alignment gap) Instance size: 512 bytes Space losses: 36 bytes internal + 4 bytes external = 40 bytes total在代码运行时可使用jstack -l 进程id,查看具体锁信息,结论synchronization锁定的是JVM中运行的对象,静态方法锁定的是Class对象,代码块锁定的是括号中的对象。参考资料https://github.com/openjdk/jolhttps://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html
Apcahe Calcite 简介Apcahe Calcite是一个基于SQL语法的查询引擎,可以使用sql语句查询多种数据源,例如csv,mongodb,redis等等,主要功能标准SQL解析和校验:行业标准的 SQL 解析器、验证器和 JDBC 驱动程序。查询优化:在关系代数中表示查询,使用计划规则进行转换,并根据成本模型进行优化。任何数据,任务地方:连接到第三方数据源,浏览元数据,并通过将计算推送到数据进行优化。Calcite是一个sql引擎,通常使用是使用对应的工具包例如calcite-redis,calcite-mongodb用于异构数据源sql查询支持。sql解析只是其中一个部分,下面的代码仅仅是解析sql部分的demo。sql解析示例 String sqlStr = "SELECT\n" + "\tsu.dept_id `deptId`,\n" + "\tsu.user_id,\n" + "\tsr.role_id,\n" + "\tsu.user_name,\n" + "\tsd.dept_name,\n" + "\tsr.role_name\n" + "FROM\n" + "\tsys_user AS su\n" + "JOIN sys_dept sd ON su.dept_id = sd.dept_id\n" + "JOIN sys_user_role sur ON sur.user_id = su.user_id\n" + "JOIN sys_role sr ON sur.role_id = sr.role_id\n" + "WHERE\n" + "\tsd.dept_name = '研发部门'\n" + "\tand su.user_name = 'admin'\n" + "\tand su.dept_id = 103\n" + "\tor sr.role_name = '超级管理员'\n" + "ORDER BY\n" + "\tsd.create_time DESC"; SqlNode sqlNode = SqlParser.create(sqlStr, SqlParser.config().withLex(Lex.MYSQL)).parseQuery(); sqlNode.accept(new SqlBasicVisitor<String>() { public String visit(SqlCall call) { if (call.getKind().equals(SqlKind.SELECT)) { SqlSelect select = (SqlSelect) call; log.info("--------------查询列名----------------------------------------"); select.getSelectList().forEach(colum -> { if (SqlKind.AS.equals(colum.getKind())) { SqlBasicCall basicCall = (SqlBasicCall) colum; log.info("{} as {}", basicCall.getOperandList().get(0).toString(), basicCall.getOperandList().get(1).toString()); } else if (SqlKind.IDENTIFIER.equals(colum.getKind())) { log.info(colum.toString()); } }); log.info("--------------From Table Info----------------------------------------"); select.getFrom().accept(new SqlBasicVisitor<String>() { public String visit(SqlCall call) { if (call.getKind().equals(SqlKind.JOIN)) { SqlJoin join = (SqlJoin) call; log.info("join.getRight:{},join.getCondition{}", join.getRight().toString(), join.getCondition().toString()); if (!join.getLeft().getKind().equals(SqlKind.JOIN)) { log.info("join.getLeft:{}", join.getLeft().toString()); } } return call.getOperator().acceptCall(this, call); } }); log.info("--------------Where Info-----------------------------------------"); select.getWhere().accept(new SqlBasicVisitor<String>() { public String visit(SqlCall call) { if (call.getKind().equals(SqlKind.AND) || call.getKind().equals(SqlKind.OR)) { SqlBasicCall sql = (SqlBasicCall) call; SqlBasicCall left = (SqlBasicCall) sql.getOperandList().get(0); SqlBasicCall right = (SqlBasicCall) sql.getOperandList().get(1); log.info("kind:{},right:{}", sql.getKind(), right); if (!left.getKind().equals(SqlKind.AND) && !left.getKind().equals(SqlKind.OR)) { log.info("left:{}", left); } } return call.getOperator().acceptCall(this, call); } }); log.info("--------------增加查询条件----------------------------------------"); try { SqlNode condition = SqlParser.create("1=1").parseExpression(); SqlNode where = SqlUtil.andExpressions(select.getWhere(),condition); select.setWhere(where); } catch (SqlParseException e) { throw new RuntimeException(e); } log.info("语句:{}", select); } return call.getOperator().acceptCall(this, call); } });SqlUtil.andExpressions:拼接查询条件。SqlParser:sql转换器,将sql字符串转换为sql语法树SqlNode:sql语法树基础元素SqlParserPos:为当前元素在sql语法树中位置SqlKind:节点类型SqlCall:语句节点,用于判断语句类型SqlBasicCall:最小单位的完整sql节点例如AS,JOIN,AND等SqlBasicVisitor:访问器,泛型为返回值,直接返回即获得指定元素,包含多个访问器其中参数为SqlCall的可以访问完整元素所以使用此方法。T visit(SqlCall call):访问SqlCall元素,返回值call.getOperator().acceptCall(this, call)递归调用。参考资料https://calcite.apache.org/docs/tutorial.html
JSqlParser 简介JSqlParser是一个SQL语句解析器,支持常见数据库包含Oracle、SqlServer、MySQL、PostgreSQL,可以直接操作由语句生成的对象并生成新的sql,也可以使用访问者模式访问sql转换而来的对象使用 String sqlStr = "SELECT\n" + "\tsu.dept_id `deptId`,\n" + "\tsu.user_id,\n" + "\tsr.role_id,\n" + "\tsu.user_name,\n" + "\tsd.dept_name,\n" + "\tsr.role_name\n" + "FROM\n" + "\tsys_user AS su\n" + "JOIN sys_dept sd ON su.dept_id = sd.dept_id\n" + "JOIN sys_user_role sur ON sur.user_id = su.user_id\n" + "JOIN sys_role sr ON sur.role_id = sr.role_id\n" + "WHERE\n" + "\tsd.dept_name = '研发部门'\n" + "\tand su.user_name = 'admin'\n" + "\tand su.dept_id = 103\n" + "\tor sr.role_name = '超级管理员'\n" + "ORDER BY\n" + "\tsd.create_time DESC"; Select querySql = (Select)CCJSqlParserUtil.parse(sqlStr); querySql.getSelectBody().accept(new SelectVisitorAdapter () { @Override public void visit(PlainSelect plainSelect) { log.info("--------------查询列名----------------------------------------"); plainSelect.getSelectItems().stream().forEach(selectItem -> { selectItem.accept(new SelectItemVisitorAdapter() { @Override public void visit(SelectExpressionItem selectExpressionItem) { log.info(selectExpressionItem.getExpression().toString()); if (selectExpressionItem.getAlias()!=null) { log.info("列别名 {}",selectExpressionItem.getAlias().getName()); } } }); }); log.info("--------------From Table Info----------------------------------------"); log.info(plainSelect.getFromItem().toString()); if (plainSelect.getFromItem().getAlias()!=null) { log.info("表别名"+plainSelect.getFromItem().getAlias().getName()); } log.info("--------------Join Table Info----------------------------------------"); plainSelect.getJoins().stream().forEach(join -> { log.info(join.toString()); log.info("关联表:{} ",join.getRightItem()); if (join.getRightItem().getAlias()!=null) { log.info("关联表别名:{}",join.getRightItem().getAlias().getName()); } log.info("关联条件:{}",join.getOnExpression().toString()); }); log.info("--------------Where Info----------------------------------------"); plainSelect.getWhere().accept(new ExpressionVisitorAdapter() { @Override public void visitBinaryExpression(BinaryExpression expr) { log.info("表达式:{}",expr.toString()); log.info("表达式左侧:{}",expr.getLeftExpression().toString()); log.info("表达式右侧:{}",expr.getRightExpression().toString()); } }); log.info("--------------增加查询条件----------------------------------------"); try { plainSelect.setWhere(new AndExpression(CCJSqlParserUtil.parseCondExpression("1=1"),plainSelect.getWhere())); } catch (JSQLParserException e) { throw new RuntimeException(e); } } }); log.info("语句:{}",querySql.toString());参考资料https://github.com/JSQLParser/JSqlParser/wiki
Linuxtop 刷新打印当前占比最高进程-c :显示进程完整路径-p:后接进程号打印进程信息 -H:线程模式,配合-p使用查看进程下线程占比top之后按m : 按内存使用排序按c : 按CPU使用排序按f : 调整展示列及列顺序printf "%x" 线程id 将线程id转换为16进制配合top及jstack 使用查找具体代码free 打印当前资源占用情况free -h:以方便阅读的方式展示资源单位PS 打印进程信息参数繁多建议使用man ps查看文档常用的打印格式有两种ps -ef:包含用户,进程id,CPU使用占比等ps aux:除了上述还包含内存占比好用的命令# 打印内存使用前十的进程 ps axo %mem,pid,euser,cmd | sort -nr | head -10 # 打印cpu使用前十的进程 ps -aeo pcpu,user,pid,cmd | sort -nr | head -10grep [选项] [参数] 查找符合条件的字符串通常在一个查询命令后通过"|"连接grep命令过滤数据-A:查看匹配字符后n行-B:查看匹配字符前n行-E: 使用正则过滤JAVAjinfo [选项] 进程id-flags : 查看所有jvm参数设置,例如内存大小等-sysprops : 查看进程对应应用详情jstat [选项] 进程id 时间间隔毫秒毫秒 打印次数-gcutil : 显示垃圾收集信息jmap [选项] 进程id-heap : 打印堆统计信息-dump:导出堆栈数据至文件, jmap -dump:format=b,file=-histo:live:打印存活对象统计查看堆内对象的分布 Top 50jmap -histo:live 30628 | sort -n -r -k2 | head -n 50jstack [选项] 进程id打印栈信息查找进程下某个线程栈信息jstack 进程id | grep -A 10 线程id参数参数说明-Xms1G初始堆内存大小-Xmx1G最大堆内存大小-XX:+PrintGCDetails打印gc详细日志-XX:+PrintGCDateStamps打印GC日期格式时间戳-Xloggc:完整文件路径gc日志文件设置-XX:HeapDumpOnOutOfMemoryError当内存溢出时生成堆快照-XX:HeapDumpPath=目录指定生成堆快照目录参考资料https://developer.aliyun.com/article/727625?spm=5176.8068049.0.0.7f0d6d19%20WJXuiS#slide-4https://g-ghy.github.io/linux-command/c/ps.html
Spring ApplicationEvent 使用ApplicationEvent为Spring的事件基类,可通过@EventListener或实现ApplicationListener接口进行监听监听及触发事件自定义事件import org.springframework.context.ApplicationEvent; public class CustomAnnotationEvent extends ApplicationEvent { private static final long serialVersionUID = -4180050946434009635L; /** * 类型 */ private String type; /** * 构造方法 * * @param source 事件源 * @param type 类型 */ public CustomAnnotationEvent(Object source, String type) { super(source); this.type = type; } /** * 获取类型 * * @return 获取类型 */ public String getType() { return type; } }抛出事件//spring注入 @Autowired private ApplicationContext applicationContext; ... CustomAnnotationEvent event = new CustomAnnotationEvent(this, "annotation"); applicationContext.publishEvent(event);监听事件@EventListener public void listenCustomAnnotationEventAll(CustomAnnotationEvent event) { log.info("listenCustomAnnotationEventAll:{}", JSONUtil.toJsonStr(event)); }监听事件及抛出事件的类需为spring管理的bean其它@EventListener(condition = "#event.type eq 'anycAnnotation' ") @Async public void listenCustomAnnotationAsyncEvent(CustomAnnotationEvent event) { log.info("listenCustomAnnotationEvent1:{}", JSONUtil.toJsonStr(event)); }异步事件:在方法上增加@Async注解则会将事件处理转为异步处理,异常及耗时不会影响抛出事件的方法,需在启动类中增加@EnableAsync开启此功能条件过滤:@EventListener注解中condition为SpEL表达式,可访问参数中的属性进行判断是否处理此事件同时监听多个事件:可使用@EventListene注解中classes条件扩充监听的事件@EventListener(classes = { CustomAnnotationEvent.class, CustomAsyncErrorEvent.class,CustomAsyncEvent.class, CustomMetohEvent.class}) public void handleMultipleEvents(ApplicationEvent event) { log.info("handleMultipleEvents:{}", JSONUtil.toJsonStr(event)); }标准事件事件类说明ContextRefreshedEventApplicationContext初始化或刷新时发布ContextStartedEvent手动调用start()方法时发布ContextStoppedEvent手动调用stop()方法时发布ContextClosedEventApplicationContext关闭时发布另还有很多内置事件可通过查看ApplicationEvent子类来查看,例如SessionCreationEvent,KafkaEvent,RedisKeyspaceEvent等参考资料https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#context-functionality-eventshttps://www.baeldung.com/spring-context-events
Spring Boot整合Kafkapom.xml<dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency>默认使用配置prop新增kafka配置 详细配置可参考org.springframework.boot.autoconfigure.kafka.KafkaProperties中属性 生产者,消费者默认序列化都是String格式message#kafka默认消费者配置 spring.kafka.consumer.bootstrap-servers=10.111.7.124:9092 spring.kafka.consumer.enable-auto-commit=true spring.kafka.consumer.auto-offset-reset=earliest #kafka默认生产者配置 spring.kafka.producer.bootstrap-servers=10.111.7.124:9092 spring.kafka.producer.acks=-1 spring.kafka.client-id=kafka-producer spring.kafka.producer.batch-size=5使用//生产者 @Resource private KafkaTemplate kafkaTemplate; public void send() { HashMap<String, String> map = new HashMap<>(); map.put("sendType","send"); kafkaTemplate.send("test01", JSONUtil.toJsonStr(map)); }//消费者 @Slf4j @Component public class KafkaConsumer { @KafkaListener(topics = "test01",groupId = "group01") public void listen(String message) { log.info("message:{}",message); } }使用Json格式化配置自定义消费者工厂: 配置全部消费者参数,包含转换器,使用此项可删除转换器配置2**转换器配置2:**仅配置json转换器,不需要配置全部消费者参数时可删除自定义消费者配置addTrustedPackages("*"):默认Json转换仅信任java.util,java.lang包下类,增加后不进行类型检查@Configuration public class KafkaCustomConfig { @Value("${spring.kafka.consumer.bootstrap-servers}") private String bootstrapServers; //自定义消费者工厂 @Bean("customContainerFactory") public ConcurrentKafkaListenerContainerFactory customContainerFactory() { Map<String, Object> props = new HashMap<>(); props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "15000"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class); props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 5); props.put(ConsumerConfig.GROUP_ID_CONFIG, "customGroup01"); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); //转换器配置1 DefaultKafkaConsumerFactory consumerFactory = new DefaultKafkaConsumerFactory(props); JsonDeserializer jsonDeserializer = new JsonDeserializer(); jsonDeserializer.getTypeMapper().addTrustedPackages("*"); consumerFactory.setValueDeserializer(jsonDeserializer); //指定使用DefaultKafkaConsumerFactory ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory(); factory.setConsumerFactory(consumerFactory); //设置可批量拉取消息消费,拉取数量一次3,看需求设置 factory.setConcurrency(3); factory.setBatchListener(true); return factory; } //转换器配置2 @Bean public RecordMessageConverter converter() { ByteArrayJsonMessageConverter converter = new ByteArrayJsonMessageConverter(); DefaultJackson2JavaTypeMapper typeMapper = new DefaultJackson2JavaTypeMapper(); typeMapper.setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence.TYPE_ID); typeMapper.addTrustedPackages("*"); converter.setTypeMapper(typeMapper); return converter; } /** * 不使用spring boot的KafkaAutoConfiguration默认方式创建的KafkaTemplate,重新定义 * 与默认配置只能存在一个 * @return */ @Bean("custiomKafkaTemplate") public KafkaTemplate custiomKafkaTemplate() { Map<String, Object> props = new HashMap<>(); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); //0 producer不等待broker同步完成的确认,继续发送下一条(批)信息 //1 producer要等待leader成功收到数据并得到确认,才发送下一条message。 //-1 producer得到follwer确认,才发送下一条数据 props.put(ProducerConfig.ACKS_CONFIG, "1"); props.put(ProducerConfig.BATCH_SIZE_CONFIG, 5); props.put(ProducerConfig.LINGER_MS_CONFIG, 500); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); DefaultKafkaProducerFactory produceFactory = new DefaultKafkaProducerFactory(props); return new KafkaTemplate(produceFactory); } }使用vo@Data public class TestVo { private String key; private String value; }生产者//注入自定义KafkaTemplate @Resource(name = "custiomKafkaTemplate") private KafkaTemplate custiomKafkaTemplate; public void customSend() { TestVo vo = new TestVo(); vo.setKey("sendType"); vo.setValue("send"); custiomKafkaTemplate.send("custom04", vo); }消费者containerFactory:指定自定义消费者工厂beanName@Slf4j @Component public class KafkaConsumer { //自定义消费者 @KafkaListener(topics = "custom04",containerFactory = "customContainerFactory") public void customListen(TestVo message) { log.info("message:{}",message.toString()); } //仅自定义转换器 @KafkaListener(topics = "custom04",groupId = "custom04Group") public void customListen(TestVo message) { log.info("message:{}",message.toString()); } }参考资料https://github.com/spring-projects/spring-kafka/tree/master/sampleshttps://blog.csdn.net/u012045045/article/details/111034500https://www.tutorialspoint.com/spring_boot/spring_boot_apache_kafka.htm
Spring Boot 监听UDP消息依赖配置 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifactId> </dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-ip</artifactId> </dependency>java配置 import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.IntegrationFlows; import org.springframework.integration.ip.udp.UnicastReceivingChannelAdapter; import org.springframework.integration.ip.udp.UnicastSendingMessageHandler; import org.springframework.messaging.MessageHandler; @Configuration public class UdpConfig { /** * udp传输端口 */ @Value("${udp.port}") private Integer udpPort; /** * 接收的消息配置 * 指定接收的端口 * @param udpClient * @return */ @Bean public IntegrationFlow processUniCastUdpMessage(MessageHandler udpClient) { return IntegrationFlows .from(new UnicastReceivingChannelAdapter(udpPort)) .handle(udpClient) .get(); } /** * 发送消息配置 * 指定ip和端口 * @return */ @Bean public UnicastSendingMessageHandler sending(){ return new UnicastSendingMessageHandler("localhost", udpPort); } }使用监听消息: udpClient名称对应配置中beanimport lombok.extern.slf4j.Slf4j; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; import org.springframework.stereotype.Component; @Slf4j @Component("udpClient") public class UdpClient implements MessageHandler { @Override public void handleMessage(Message<?> message) throws MessagingException { String payload = new String((byte[]) message.getPayload()); log.info("接收到消息-payload:{}", payload); } }发送消息import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.ip.udp.UnicastSendingMessageHandler; import org.springframework.integration.support.MessageBuilder; import org.springframework.stereotype.Service; @Slf4j @Service public class UdpServer { @Autowired private UnicastSendingMessageHandler sendingMessageHandler; public void sendMessage(String message) { log.info("发送UDP: {}", message); sendingMessageHandler.handleMessage(MessageBuilder.withPayload(message).build()); } }
Publish over SSH介绍Jenkins发布插件,支持通过ssh将文件发送至远端服务器,在远端服务器执行命令,可在maven构建前,构建后执行. 全局配置位置:Dashboard-configuration-Publish over SSH-SSH Servers配置远端服务器,支持多个服务器SSH Server:配置服务器别名,ip,用户名密码等信息, Remote Directory:远端工作目录,拷贝文件会以此目录为工作空间根目录项目配置位置:Build新增Send files or execute commands over SSH,增加在maven构建之后.SSH Server-Name:全局配置中配置的远端服务器点击又下角"Advanced"按钮勾选"Verbose output in console"可将命令执行结果返回至Jenkins便于调试Source files:需要发送到远端的文件,相对路径,基于当前项目的工作空间Remove prefix:拷贝至远端需删除的前缀Remote directory:相对路径远端服务存储的文件目录,基于全局配置中的Remote DirectoryExec command:传输完成之后执行的命令,可进行文件备份,服务重启等操作下面的命令是备份文件,重启服务,重启服务的脚本在服务器上,如有需要也可以在此处完成.#!/bin/bashbaseDir="/home/workspace/xxxProject/"deployDir=$baseDir"lib/"historyDir=$baseDir"history/"binDir=$baseDir"bin/"jarName="xxxProject.jar"jarName_prefix="xxxProject"jarName_suffix=".jar"dateStr=`date +%s`echo $dateStrcopyFileName=$jarName_prefix$dateStr$jarName_suffixecho $copyFileNamecp -i $deployDir$jarName $historyDir$copyFileNameecho "Backup complete"mv -f $historyDir$jarName $deployDir$jarNamecd $binDir./xxxProject restart
redis集群-哨兵模式(sentinel)哨兵模式:基于redis主从复制,增加sentinel服务用于监听服务状态,并在发生异常时重新选取主服务器 使用版本,redis6.25配置文件下面只列出主要配置,需查看详细配置请查看关联项目 redis登陆密码如配置则需要统一master配置requirepass 登陆密码 # 发生故障时会重新指定主服务器,所以需要指定密码 masterauth 主服务器登陆密码slave配置在docker环境执行,避免重复修改配置文件所以使用redis01(机器名),此处别名对应docker启动时指定的别名requirepass 登陆密码 masterauth 主服务器登陆密码 # 指定主服务器 replicaof redis01 6379sentinel配置mymaster 为sentinel监控的别名,sentinel monitor mymaster redis01 6379 2 sentinel auth-pass mymaster sbsb1234 # 6.2之后才支持使用机器名默认是关闭,此处打开 SENTINEL resolve-hostnames yesdocker 启动结构为3个redis节点,1主2从,3个sentinel服务准备环境# 下载redis最新镜像 docker pull redis #新建一个网络使其它节点连接此网络,避免通信问题 docker network create testnet启动redis服务命令说明 *. -p 6380:6379 指定外内端口 *. --name redis01 指定容器名称 *. -v /home/docker/redis/master.conf:/etc/redis/redis.conf 映射配置文件 *. --privileged=true 提升docker容器权限避免容器中操作权限不够 *. --network testnet 指定容器使用网络 *. --network-alias redis01 指定网络中机器名 *. -d redis 后台执行容器 *. redis-server /etc/redis/redis.conf 执行的cmd命令,指定配置文件否则不会使用映射配置文件docker run -p 6380:6379 --name redis01 -v /home/docker/redis/master.conf:/etc/redis/redis.conf --privileged=true --network testnet --network-alias redis01 -d redis redis-server /etc/redis/redis.conf docker run -p 6381:6379 --name redis02 -v /home/docker/redis/slave.conf:/etc/redis/redis.conf --privileged=true --network testnet --network-alias redis02 -d redis redis-server /etc/redis/redis.conf docker run -p 6382:6379 --name redis03 -v /home/docker/redis/slave.conf:/etc/redis/redis.conf --privileged=true --network testnet --network-alias redis03 -d redis redis-server /etc/redis/redis.conf启动完成后可使用redis客户端连接或使用命令行验证主从是否正常# 查看主从配置信息 info replication启动sentinel服务命令说明 *. redis-server /etc/redis/sentinel.conf --sentinel 表示启动sentinel服务docker run -p 26379:26379 --name sentinel01 -v /home/docker/redis/sentinel.conf:/etc/redis/sentinel.conf --privileged=true --network testnet --network-alias sentinel01 -d redis redis-server /etc/redis/sentinel.conf --sentinel docker run -p 26380:26379 --name sentinel02 -v /home/docker/redis/sentinel.conf:/etc/redis/sentinel.conf --privileged=true --network testnet --network-alias sentinel02 -d redis redis-server /etc/redis/sentinel.conf --sentinel docker run -p 26381:26379 --name sentinel03 -v /home/docker/redis/sentinel.conf:/etc/redis/sentinel.conf --privileged=true --network testnet --network-alias sentinel03 -d redis redis-server /etc/redis/sentinel.conf --sentinel启动成功后可在主服务器控制台输入如下命令验证# 执行休眠120s的任务 DEBUG SLEEP 120s # 再次查看主从信息查看是否已一切换为从服务器 info replication相关项目https://gitee.com/MeiJM/spring-cram/tree/master/redis/redisConfig参考资料https://www.cnblogs.com/shenh/p/9714547.htmlhttps://cloud.tencent.com/developer/article/1343834
AbstractQueuedSynchronizerAQS是一个内部维护了先进先出队列+标识(数字)的模型,标识使用CAS模式修改,作为多线程工具的基础组件属性属性名说明volatile Node head为头节点会清理Node中关联的pre,thread避免GC不回收volatile Node tail尾节点volatile int state0为空闲其它组件按需使用,使用cas来赋值,Thread exclusiveOwnerThread持有线程state为volatile的int,不同的业务场景按需实现,独占模式:ReentrantLock.Sync中state为0表示未锁定>0表示被几个线程持有ThreadPoolExecutor.Worker中state为0表示未执行共享模式:CountDownLatch.Sync中state为初始化时指定,表示有多少个线程可持有,Semaphore.Sync中state与CountDownLatch相同混合模式:ReentrantReadWriteLock.Sync中state为共享锁+独占锁组合 通过位运算16位来分割,最大的读锁写锁个数为65535关键方法独占模式持有资源:acquireacquire: 独占模式获取,独占模式即只有一个线程可更新state.忽略中断标识,在获取之后响应中断。acquireInterruptibly:独占模式获取,线程标识为中断则抛出异常public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }tryAcquire:子类按需实现,使用独占模式更新state,增加state,成功返回true失败返回false中断后不会正确响应park,所以需要重置线程中断标识,并在unpark之后进行中断补偿释放资源:releaserelease:以独占模式释放资源public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }tryRelease:子类按需实现,使用独占模式更新state,减少state,并处理对应独占线程共享模式持有资源:acquireSharedacquireShared 共享模式获取,忽略中断线程,在获取之后相应中断。acquireSharedInterruptibly 共享模式获取,响应中断,线程中断则抛出异常。public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }tryAcquireShared:子类按需实现,对返回值有如下要求:负值:失败。 零:共享模式下的获取成功,但是没有后续共享模式下的获取可以成功。 正值: 如果共享模式下的获取成功并且后续共享模式下的获取也可能成功,则为正值,在这种情况下,后续的等待线程必须检查可用性。 (对三个不同返回值的支持使该方法可以在仅有时进行获取的情况下使用。)成功后,就已经获取了此对象。释放资源:releaseSharedreleaseShared:以共享模式释放资源public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }tryReleaseShared:子类按需实现,使用共享模式更新state,减少state其它关键方法检查并更新节点状态:shouldParkAfterFailedAcquire在park线程之前的判断,当前置节点为取消时更新前置节点private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }唤醒后续节点:unparkSuccessor唤醒后续节点,并清理取消的节点,private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }共享模式设置队列头:setHeadAndPropagate共享模式下多个线程同时持有资源,头节点会频繁变化需要及时释放资源private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); /* * Try to signal next queued node if: * Propagation was indicated by caller, * or was recorded (as h.waitStatus either before * or after setHead) by a previous operation * (note: this uses sign-check of waitStatus because * PROPAGATE status may transition to SIGNAL.) * and * The next node is waiting in shared mode, * or we don't know, because it appears null * * The conservatism in both of these checks may cause * unnecessary wake-ups, but only when there are multiple * racing acquires/releases, so most need signals now or soon * anyway. */ if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } }Node属性名说明int waitStatus1:CANCELLED,表示当前的线程被取消;<br/>-1:SIGNAL,后续节点需要unpark;<br/>-2:CONDITION,表示当前节点在等待condition,也就是在condition队列中;<br/>-3:PROPAGATE,A releaseShared应该传播到其他节点。 在doReleaseShared中对此进行了设置(仅适用于头节点),以确保传播继续进行,即使此后进行了其他操作也是如此。 <br/> 0:表示当前节点在sync队列中,等待着获取锁。Node prev前驱节点,比如当前节点被取消,那就需要前驱节点和后继节点来完成连接。Node next后继节点。Thread thread入队列时的当前线程。Node nextWaiter为NULL表示为独占模式PROPAGATE:共享模式中会通过状态是否小于0来判断是否需要唤醒后续节点,共享模式下多个线程可同时持有state变更,waitStatus会频繁从0切换为SIGNAL,区分SIGNAL增加的中间状态所以称为传播值
ConditionObject属性说明Node firstWaiter头节点Node lastWaiter尾节点为Condition接口实现,Condition的目的主要是替代Object的wait,notify,notifyAll方法的,它是基于Lock实现的.(而Lock是来替代synchronized方法).结构使用时序图关键方法阻塞线程:await对应Object.wait(),通过AQS机制释放锁定的资源,终止当前线程,恢复后使用AQS独占模式重新锁定资源acquireQueued:此时node节点已转换为AQS中节点public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter(); long savedState = fullyRelease(node); int interruptMode = 0; while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }唤醒线程:signaltransferForSignal转换节点后await()中acquireQueued(node,savedState)操作的节点已是AQS中的节点isHeldExclusively:子类实现.判断是否独家持有public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); }
生命周期相关方法interrupt():实例方法将线程中断标识设置为true,如线程在wait/sleep则报java.lang.InterruptedExceptioninterrupted():静态方法,Thread.interrupted()重置当前线程中断标识isInterrupted(): 实例方法获取当前线程中断标识join():中断当前线程,等待调用线程结束后再执行Thread.sleep(millis):当前线程休眠x毫秒,不释放持有锁LockSupport.park():中断当前线程,不释放持有锁Object.wait(millis):当前线程等待x秒,不传递则无限期等待,释放持有锁线程变量ThreadLocal:同线程内共享,无法传递至子线程生命周期:在设置第一个值时通过Thread.currentThread()与当前线程中threadLocals进行关联,并初始化一个ThreadLocalMap进行存储。ThreadLocalMap中存储内容的Entry使用的是WeakReference弱引用,便于垃圾回收。在非线程池情况下由于key为弱引用在代码中没有继续使用时(强引用去掉后只剩弱引用),会被gc回收.get方法会清理不存在的key对应的value,保障了内存回收在线程池中使用时由于线程不会被回收,Thread这个强引用不会被清理,所以存在内存溢出风险。 可在使用完成之后通过remove方法清理来规避这个隐患。使用static初始化时也存在生命周期过长存在内存溢出隐患InheritableThreadLocal:只有在创建线程池时会初始化,所以线程池场景会导致赋值逻辑混乱重写了childValue(),getMap(Thread),createMap(Thread t, T firstValue) 来实现子线程的内容赋值ThreadLocal/InheritableThreadLocal 使用流程: TransmittableThreadLocal:可传递至子线程,子线程独立操作,新开线程也会获取新的值,需配合TtlRunnable或修改java启动参数来使用整个过程的官方完整时序图: 依据官方时序图自己的整理:
文件位置配置文件位置/etc/my.cnf或/etc/mysql/my.cnf配置文件内容[mysqld] # 标识机器id,在binlog中会记录此信息,在slave机器中也回记录对应的master主机server_id server_id=1 # 主库配置 # binlog名称,表示开启binlog log-bin=mysql-bin # binlog忽略的库,可配置多个 binlog-ignore-db=mysql binlog-ignore-db=sys binlog-ignore-db=information_schema # binlog记录的库 binlog-do-db=mycattest # binlog记录的记录类型 可选ROW/STATEMENT/MIXED binlog-format=STATEMENT #从库配置 #启用中继日志,中继日志名称 relay-log=mysql-relay查询环境变量语句:show variables like '%relay%';binlog-format参数说明值说明优点缺点ROW从数据层面记录变化确保数据一致性记录日志过多,且效率底例如批量更新会记录所有更新后的数据STATEMENT记录修改的SQL节省空间,性能高无法正确解析依赖上下文的环境变量,及数据库函数,例如@@hostname,UUID()等MIXED依据每条sql判断使用哪种模式记录日志综合了两个模式优点尽量保证了数据一致性增加了复杂性企业场景如何选择binlog模式 1、互联网公司,使用MySQL的功能相对少(存储过程、触发器、函数) 选择默认的语句模式,Statement Level(默认) 2、公司如果用到使用MySQL的特殊功能(存储过程、触发器、函数) 则选择Mixed模式 3、公司如果用到使用MySQL的特殊功能(存储过程、触发器、函数)又希望数据最大化一直,此时最好选择Row level模式Mixed切换为ROW模式记录场景当 DML 语句更新一个 NDB 表时;当函数中包含 UUID() 时;2 个及以上包含 AUTO_INCREMENT 字段的表被更新时;执行 INSERT DELAYED 语句时;用 UDF 时;视图中必须要求运用 row 时,例如建立视图时使用了 UUID() 函数;事物隔离级别较高时默认事物隔离级别为REPEATABLE-READ参考资料https://i4t.com/213.html https://www.cnblogs.com/zhoujinyi/p/5436250.html
Activiti7 驳回任务下面操作在多分支情况下没有做详细判断,使用需注意.操作说明通过执行实例id获取bpmnModel通过历史记录找到上一条任务并找到任务定义通过任务定义找到上一个任务入口设置当前执行实例的当前节点,并执行流程引擎计算下一节点.package com.meijm.activiti.common.flow; import cn.hutool.core.collection.CollectionUtil; import org.activiti.bpmn.model.BpmnModel; import org.activiti.bpmn.model.FlowElement; import org.activiti.bpmn.model.FlowNode; import org.activiti.engine.delegate.TaskListener; import org.activiti.engine.delegate.event.ActivitiEventDispatcher; import org.activiti.engine.delegate.event.ActivitiEventType; import org.activiti.engine.delegate.event.impl.ActivitiEventBuilder; import org.activiti.engine.history.HistoricTaskInstance; import org.activiti.engine.impl.HistoricTaskInstanceQueryImpl; import org.activiti.engine.impl.HistoricTaskInstanceQueryProperty; import org.activiti.engine.impl.cmd.DeleteProcessInstanceCmd; import org.activiti.engine.impl.context.Context; import org.activiti.engine.impl.identity.Authentication; import org.activiti.engine.impl.interceptor.Command; import org.activiti.engine.impl.interceptor.CommandContext; import org.activiti.engine.impl.persistence.entity.ExecutionEntity; import org.activiti.engine.impl.persistence.entity.HistoricTaskInstanceEntityManager; import org.activiti.engine.impl.persistence.entity.TaskEntity; import org.activiti.engine.impl.util.ProcessDefinitionUtil; import org.activiti.engine.task.IdentityLinkType; import java.util.List; import java.util.Map; public class RejectTask implements Command<Void> { private String taskId; private String rejectReason; private Map<String, Object> variables; public RejectTask(String taskId, Map<String, Object> variables, String rejectReason) { this.taskId = taskId; this.variables = variables; this.rejectReason = rejectReason; } @Override public Void execute(CommandContext commandContext) { TaskEntity task = commandContext.getTaskEntityManager().findById(taskId); HistoricTaskInstanceEntityManager historicTaskInstanceEntityManager = commandContext.getHistoricTaskInstanceEntityManager(); HistoricTaskInstanceQueryImpl historicTaskInstanceQuery = new HistoricTaskInstanceQueryImpl(); historicTaskInstanceQuery.processInstanceId(task.getProcessInstanceId()); historicTaskInstanceQuery.orderBy(HistoricTaskInstanceQueryProperty.START).desc(); List<HistoricTaskInstance> htiList = historicTaskInstanceEntityManager.findHistoricTaskInstancesByQueryCriteria(historicTaskInstanceQuery); if (CollectionUtil.isEmpty(htiList) || htiList.size() < 2) { DeleteProcessInstanceCmd cmd = new DeleteProcessInstanceCmd(task.getProcessInstanceId(), rejectReason); cmd.execute(commandContext); } else { // list里的第二条代表上一个任务 HistoricTaskInstance lastTask = htiList.get(1); String processDefinitionId = lastTask.getProcessDefinitionId(); BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(processDefinitionId); // 得到上个节点的开始位置 FlowNode lastFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(lastTask.getTaskDefinitionKey()); FlowElement begin = lastFlowNode.getIncomingFlows().get(0).getSourceFlowElement(); commandContext.getProcessEngineConfiguration().getListenerNotificationHelper().executeTaskListeners(task, TaskListener.EVENTNAME_COMPLETE); if (Authentication.getAuthenticatedUserId() != null && task.getProcessInstanceId() != null) { ExecutionEntity processInstanceEntity = commandContext.getExecutionEntityManager().findById(task.getProcessInstanceId()); commandContext.getIdentityLinkEntityManager().involveUser(processInstanceEntity, Authentication.getAuthenticatedUserId(), IdentityLinkType.PARTICIPANT); } ActivitiEventDispatcher eventDispatcher = Context.getProcessEngineConfiguration().getEventDispatcher(); if (eventDispatcher.isEnabled()) { eventDispatcher.dispatchEvent(ActivitiEventBuilder.createEntityWithVariablesEvent(ActivitiEventType.TASK_COMPLETED, task, variables, false)); } task.setVariables(variables); commandContext.getTaskEntityManager().deleteTask(task, rejectReason, false, false); ExecutionEntity executionEntity = commandContext.getExecutionEntityManager().findById(task.getExecutionId()); executionEntity.setCurrentFlowElement(begin); commandContext.getAgenda().planTakeOutgoingSequenceFlowsOperation(executionEntity, true); } return null; } }源文件位置com.meijm.activiti.common.flow.RejectTask地址:https://gitee.com/MeiJM/spring-cram/tree/master/activiti参考资料https://segmentfault.com/a/1190000013952695
例子简介将spring-cache,spring-session存储配置为redis,并提供三个例子分别对应cache,session以及redisTemplate的读写操作,仅保留了核心代码,其它配置请参考官方文档.配置pom<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>application.properties# 使用redis存储session spring.session.store-type=redis # 单位时间秒 server.servlet.session.timeout= 1800 # session存储名称 spring.session.redis.namespace=spring:session # redis地址配置, spring.redis.host=localhostjava配置@Configuration @EnableCaching public class RedisConfig { /** * 固定方法名称,否则session序列化不会使用json格式 * org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration#setDefaultRedisSerializer(org.springframework.data.redis.serializer.RedisSerializer) * @return */ @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer() { //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式) Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); //序列化配置 ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); serializer.setObjectMapper(mapper); return serializer; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // 配置序列化 RedisCacheConfiguration config = RedisCacheConfiguration .defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer)) // 值序列化方式 简单json序列化 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(springSessionDefaultRedisSerializer())) //过期时间 .entryTtl(Duration.ofMinutes(5)); return RedisCacheManager .builder(factory) .cacheDefaults(config) // .withCacheConfiguration("cacheName",customConfig) .build(); } @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); template.setValueSerializer(springSessionDefaultRedisSerializer()); //使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } }session配置说明: 默认session存储redis使用的是JdkSerializationRedisSerializer,要替换则需要手动注入springSessionDefaultRedisSerializercache配置说明: 使用withCacheConfiguration可针对缓存名称定制配置其它: 官方提供的json格式序列化有Jackson2JsonRedisSerializer和GenericJackson2JsonRedisSerializer,GenericJackson2JsonRedisSerializer会将类名存储在json字符串中所以可读性更高,但如果序列化bean不规范则会报错,例如没有空构造函数,get,set不全等.相关代码https://gitee.com/MeiJM/spring-cram/tree/master/redis参考资料https://docs.spring.io/spring-session/docs/current/reference/html5/#custom-sessionrepository
角色说明CustomAuthenticationFilter:构建Token类并交给AuthenticationProvider进行验证,继承自AbstractAuthenticationProcessingFilter,AbstractAuthenticationProcessingFilter为UsernamePasswordAuthenticationFilter的父类,封装了登陆过程用到的常用内容及方法.也可以不继承此类完核心功能即可.CustomAuthenticationProvider:对支持的token进行校验与shiro中Realm类似,区别是Security将授权与认证合并了.CustomAuthenticationToken:自定义token,存储自定义内容,程序中使用SecurityContextHolder.getContext().getAuthentication()来获取java配置过滤器配置public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public CustomAuthenticationFilter() { super(new AntPathRequestMatcher("/custom/**")); // setContinueChainBeforeSuccessfulAuthentication(true); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { CustomAuthenticationToken authentication = null; try { Enumeration<String> headers = request.getHeaders("secretKey"); String secretKey = headers.nextElement(); // 通过request中参数构建自定义token,与CustomAuthenticationProvider对应即可 authentication = new CustomAuthenticationToken(secretKey, secretKey); //设置附属信息,sessionid,ip setDetails(request, authentication); //通过Provider验证token authentication = (CustomAuthenticationToken) getAuthenticationManager().authenticate(authentication); // SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { throw new AuthenticationServiceException("secretKey认证失败",e); } return authentication; } protected void setDetails(HttpServletRequest request, CustomAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } }说明此示例是在请求头中增加参数用于认证,并没有后续的登陆成功处理所以设置了setContinueChainBeforeSuccessfulAuthenticationsetContinueChainBeforeSuccessfulAuthentication设置为true表示认证成功之后进入后续过滤,不走后续的登陆成功处理需要手动将认证结果存入SecurityContextHolder中避免后续拿不到认证信息.Provider 配置@Component public class CustomAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String secretKey = (String) authentication.getCredentials(); //自定义校验逻辑 if(!"123".equals(secretKey)){ throw new InsufficientAuthenticationException("认证错误"); } Set<String> dbAuthsSet = new HashSet<String>(); dbAuthsSet.add("customUser"); Collection<? extends GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(dbAuthsSet.toArray(new String[0])); return new CustomAuthenticationToken(secretKey, secretKey, authorities); } @Override public boolean supports(Class<?> authentication) { return CustomAuthenticationToken.class.isAssignableFrom(authentication); } }SecurityConfig配置@Order(2) @Configuration public class CustomSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAuthenticationProvider customAuthenticationProvider; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .antMatcher("/custom/**") .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER) .and().authorizeRequests().antMatchers("/custom/**").authenticated(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(customAuthenticationProvider); } @Bean public CustomAuthenticationFilter customAuthenticationFilter() throws Exception { CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); filter.setAuthenticationManager(super.authenticationManagerBean()); return filter; } }相关代码https://gitee.com/MeiJM/spring-cram/tree/master/customSecurity 中CustomSecurityConfig相关部分
状态机简介状态机是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件,摘录自这个大神的文章状态机引擎选型示例说明网上能找到的示例大多都过于复杂,所以写了一个简单的示例下面的代码定义了一个极简的状态机,仅包含两个状态(上班,下班),一个事件(通勤),通过web页面操作添加状态机及状态转换,集成了spring-statemachine-data-jpa进行持久化.SpringBoot2.x配置说明Pom.xml<dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-starter</artifactId> <version>2.2.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-data-jpa</artifactId> <version>2.2.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>java配置状态机持久化@Configuration public class StateMachineJpaConfig { /** * StateMachineRuntimePersister为状态机运行时持久化配置 * @param jpaStateMachineRepository * @return */ @Bean public StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister( JpaStateMachineRepository jpaStateMachineRepository) { return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); } /** * StateMachineService为状态状态机持久化控制,用于获取或关闭状态机 * @param stateMachineFactory * @param stateMachineRuntimePersister * @return */ @Bean public StateMachineService<String, String> stateMachineService( StateMachineFactory<String, String> stateMachineFactory, StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister) { return new DefaultStateMachineService<String, String>(stateMachineFactory, stateMachineRuntimePersister); } }状态机监听 继承StateMachineListenerAdapter重写需监听的方法即可,通过方法名可推测监听的事件,就不贴代码了.状态机配置,@EnableStateMachine为单例模式并不适合此示例所以使用@EnableStateMachineFactory@Configuration @EnableStateMachineFactory public class StateMachineConfig extends StateMachineConfigurerAdapter<String, String> { @Autowired private StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister; @Autowired private StateMachineLogListener logListener; @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withPersistence() .runtimePersister(stateMachineRuntimePersister) .and() .withConfiguration().listener(logListener); } @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("宿舍") .state("公司"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("宿舍").target("公司") .event("通勤") .and() .withExternal() .source("公司").target("宿舍") .event("通勤"); } }示例代码https://gitee.com/MeiJM/spring-cram/tree/master/statemachine参考资料https://docs.spring.io/spring-statemachine/docs/2.2.0.RELEASE/reference/#prefacehttps://segmentfault.com/a/1190000009906317
Spring Security多入口官方为了方便演示使用的是一个主类,两个内部类来实现的多入口,下面的例子将其拆分为两个配置类,两个用户方便理解.配置@Configuration public class FormSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public static PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().disable() .csrf().disable() .authorizeRequests().antMatchers("/form/**") .hasRole("USER") .and() .formLogin().successForwardUrl("/form/index"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password(passwordEncoder().encode("user")) .roles("USER"); } }说明:上面的配置为系统指定了user为默认用户,拥有USER权限,指定以form起始的路径需要校验USER权限增加formLogin,指定/form/index为登陆成功指向的页面@Order(1) @Configuration public class BasicSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Override protected void configure(HttpSecurity http) throws Exception { http .antMatcher("/basic/**") .authorizeRequests().anyRequest() .hasRole("BASIC") .and() .httpBasic(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("basic") .password(passwordEncoder.encode("basic")) .roles("BASIC"); } }说明:使用@Order(1)指定了加载顺序上面的配置为系统指定了basic为默认用户,拥有BASIC权限,指定以basic起始的路径需要校验BASIC权限增加httpBasic验证注意事项HttpSecurity配置以authorizeRequests为起始表示针对所有请求路径HttpSecurity配置以antMatcher("/basic/**")为新增一个入口FormSecurityConfig 未写@Order继承WebSecurityConfigurerAdapter中注解序号为100由于formLogin会增加默认登陆页过滤器/login所以不能使用其它路径作为起始,否则会导致默认登录页不生效如果authorizeRequests加载顺序靠前会导致后续配置的antMatcher对应的路径失效.相关代码https://gitee.com/MeiJM/spring-cram/tree/master/customSecurity参考资料https://docs.spring.io/spring-security/site/docs/5.4.1/reference/html5/#multiple-httpsecurityhttps://www.baeldung.com/spring-security-multiple-entry-pointshttps://github.com/spring-projects/spring-security/issues/5593https://github.com/mageddo/java-examples/blob/6a7dd2b/spring-security/basic-and-form-auth-together/src/main/java/com/mageddo/springsecurity/SecurityConfig.java
Activiti介绍Activiti是一个轻量级的java开源BPMN 2工作流引擎.目前以升级至7.x,支持与springboot2集成.SpringBoot配置说明pom.xml 增加activiti-spring-boot-starter并指定对应版本.<dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter</artifactId> <version>7.1.0.M6</version> </dependency> <!-- 指定数据源--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>java配置activiti-spring-boot默认集成了spring security用于权限管理如需禁用security启动类中屏蔽ActivitiSpringIdentityAutoConfiguration,再增加一个配置类即可 Application中禁用权限相关集成package com.meijm.activiti; import org.activiti.core.common.spring.identity.config.ActivitiSpringIdentityAutoConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication(exclude = {ActivitiSpringIdentityAutoConfiguration.class}) public class ActivitiApplication { public static void main(String[] args) { SpringApplication.run(ActivitiApplication.class); } }增加配置类ActivitiSpringIdentityAutoConfiguration,其中有三个方法在源码中并未查询到引用暂时不处理package com.meijm.activiti.config; import com.google.common.collect.ImmutableList; import org.activiti.api.runtime.shared.identity.UserGroupManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.List; @Configuration public class ActivitiSpringIdentityAutoConfiguration { @Bean public UserGroupManager userGroupManager() { return new UserGroupManager() { @Override public List<String> getUserGroups(String s) { return ImmutableList.of("指定用户归属组"); } @Override public List<String> getUserRoles(String s) { return null; } @Override public List<String> getGroups() { return null; } @Override public List<String> getUsers() { return null; } }; } }完成以上配置后项目启动时会创建基础的数据表,不包含历史记录等,如需要则在配置文件中增加配置即可.配置介绍.启动后可能会不创建数据表,数据库连接中增加nullCatalogMeansCurrent=true这个参数就好了,具体原因不明spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/m-activiti?serverTimezone=UTC&nullCatalogMeansCurrent=true工程启动后会自动加载/resources/processes下流程图配置文件, 支持两种格式*.bpmn20.xml,*.bpmn流程图编辑IDEA插件actiBPM: 安装后对*.bpmn后缀的文件可以直接打开流程图编辑视图,效果如下,编辑保存后会在标签中增加xmlns=""属性,会导致自动部署失败,可以手动删除此属性,也可以通过spring.activiti.check-process-definitions=false来关闭流程文件检查.Activiti BPMN visualizer: 安装后打开*.bpmn20.xml文件,右键选择"View BPMN(Activiti) Diagram"可打开流程图编辑页,效果如下图API说明集成配置好之后会在系统注入以下bean,使用时增加注入即可,最基础的是前三个bean.名称介绍RuntimeService执行时Service可以处理所有执行状态的流程例项流程控制(开始,暂停,挂起等)TaskService任务Service用于管理、查询任务,例如签收、办理、指派等RepositoryService流程仓库Service,可以管理流程仓库例如部署删除读取流程资源IdentitiServicec身份Service可以管理查询使用者、组之间的关系FormService表单Service用于读取和流程、任务相关的表单资料HistoryService历史Service用于查询所有的历史资料ManagementService引擎管理Service,和具体业务无关,主要查询引擎配置,资料库作业DynamicBpmService动态bpm服务例子下面源码中包含一个简单的流程例子,包含两个用户任务,提供了controller来操作流程/baseActiviti/start:流程启动/baseActiviti/taskList:查看指定用户的任务/baseActiviti/completeTask:完成指定任务说明流程图配置中assignee表示指定的用户由于实体中包含一些运行时内容不便于序列化所以返回时去掉了一些属性地址:https://gitee.com/MeiJM/spring-cram/tree/master/activiti参考资料https://www.activiti.org/userguide/#springSpringBoothttps://blog.csdn.net/yang_zzu/article/details/103998671https://www.cnblogs.com/tianguodk/p/9414363.htmlhttps://blog.csdn.net/qq_40887780/article/details/83588130https://www.cnblogs.com/liaojie970/p/8857710.html
Spring OAuth2 学习整理OAuth2 介绍OAuth2是目前最流行的授权机制,用来授权第三方应用,获取用户数据。博客提供的流程图中有一点需要注意的是C步骤是用户参与完成验证,验证之后Client拿到对应的Access Token 再进行后续操作.这时Client可以是第三方服务器或者浏览器js存储. OAuth运行流程 说明spring官方发布了blog ,最新版的SpringSecurity已经不支持创建一个授权服务器,转而提供一个标准的授权服务....下面的代码是一个单项目,包含了授权服务,资源服务.使用mybatis-plus自定义了用户信息查询和客户端信息查询.配置<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>@Configuration //资源服务配置 @EnableResourceServer //认证服务配置 @EnableAuthorizationServer public class OAuth2Config extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private SysClientDetailsService sysClientDetailsService; /** * 定义授权和令牌端点以及令牌服务 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints // 请求方式 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST) // 可重写TokenEnhancer 自定生成令牌规则 .tokenEnhancer(endpoints.getTokenEnhancer()) // 用户账号密码认证 .userDetailsService(userDetailsService) // 指定认证管理器 .authenticationManager(authenticationManager) // 是否重复使用 refresh_token .reuseRefreshTokens(false); } @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) { //是否允许客户端使用form参数提交,不开启则使用Authorization 要加在请求头中,格式为 Basic 空格 base64(clientId:clientSecret) oauthServer.allowFormAuthenticationForClients(); } /** * 配置客户端详情 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(sysClientDetailsService); } }使用流程1.获取tokenhttp://localhost:8080/oauth/token?username=admin&password=admin123&code=10&uuid=eb36f9f5946a456b8e24b2491c3db429&client_id=ruoyi&client_secret=123456&grant_type=password&scope=server2.使用access_token访问/indexhttp://localhost:8080/indexAuthorization:bearer xxxx其中xxxx为步骤1获取的access_token3.使用refresh_token获取新的access_tokenhttp://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=e3b3fc9d-2596-4977-b066-a4b01a2b48ae&client_id=ruoyi&client_secret=123456例子源码源码为ruoyi2.0版本抽出部分代码构建,新版本ruoyi已去掉oauth2的认证模式https://gitee.com/MeiJM/spring-cram/tree/master/oauth2参考资料https://www.cnblogs.com/fengzheng/p/11724625.htmlhttps://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guidehttp://www.ruanyifeng.com/blog/2014/05/oauth_2_0.htmlhttp://www.ruanyifeng.com/blog/2019/04/oauth_design.htmlhttps://gitee.com/y_project/RuoYi
java 代码增加一个工具类在jsoup获取之前调用此方法//your code SSLHelper.init(); Connection connect = Jsoup.connect(url).userAgent(USER_AGENT); connect.header("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); connect.header("Accept-Encoding", "gzip, deflate, sdch"); connect.header("Accept-Language", "zh-CN,zh;q=0.8"); connect.timeout(3000); connect.ignoreHttpErrors(true); Document doc = connect.get(); package com.bookmark.analysis.common.util; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.X509TrustManager; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; public class SSLHelper { public static String USER_AGENT = "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 5.0)"; static public void init() { try { SSLContext context = SSLContext.getInstance("TLS"); context.init(null, new X509TrustManager[]{new X509TrustManager() { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }}, new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory()); } catch (NoSuchAlgorithmException e) { } catch (KeyManagementException e) { } } }参考资料https://stackoverflow.com/questions/40742380/how-to-resolve-jsoup-error-unable-to-find-valid-certification-path-to-requested
java配置文件配置拦截器及语言环境解析@Configuration public class WebConfig implements WebMvcConfigurer { @Bean public LocaleResolver localeResolver() { //设置cookie模式处理国际化 CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver(); cookieLocaleResolver.setDefaultLocale(Locale.CHINESE); return cookieLocaleResolver; } @Override public void addInterceptors(InterceptorRegistry registry) { //指定国际化标识 LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); localeChangeInterceptor.setParamName("lang"); registry.addInterceptor(localeChangeInterceptor); } }配置国际化数据源// 1.需指定名称替换默认bean @Component("messageSource") public class DBMessageSource extends AbstractMessageSource { // 自定义service @Autowired private LanguageRepository languageRepository; // 2.国际化处理方法依据key在数据库中查找对应国际化内容 @Override protected MessageFormat resolveCode(String key, Locale locale) { LanguageEntity message = languageRepository.findByKeyAndLocale(key,locale.getLanguage()); if (message == null) { message = languageRepository.findByKeyAndLocale(key,Locale.getDefault().getLanguage()); } return new MessageFormat(message.getContent(), locale); } //3.新增方法,用于后端传参国际化 public final String getMessage(String code, @Nullable Object[] args) throws NoSuchMessageException { Locale locale = LocaleContextHolder.getLocale(); String msg = getMessageInternal(code, args, locale); if (msg != null) { return msg; } String fallback = getDefaultMessage(code); if (fallback != null) { return fallback; } throw new NoSuchMessageException(code, locale); } }其中3为自己新增的方法用于java代码中获取国际化,使用时在java代码中注入messageSource。页面<h2 th:text="#{home.welcome('xxx')}"></h2> <p th:text="#{home.info}"></p> <p th:text="#{home.changelanguage}"></p> <ul> <li><a href="?lang=en" th:text="#{home.lang.en}"></a></li> <li><a href="?lang=de" th:text="#{home.lang.de}"></a></li> <li><a href="?lang=zh" th:text="#{home.lang.zh}"></a></li> </ul>参数传递为#{key(参数……)}问题:1.Thymeleaf中[[]]为转义符导致在js代码中使用国际化并不方便完整代码https://gitee.com/MeiJM/springboot-i18n参考资料http://zhangjiaheng.cn/blog/20190320/%E4%BD%BF%E7%94%A8springboot%E8%BF%9B%E8%A1%8C%E5%9B%BD%E9%99%85%E5%8C%96%E6%97%B6%E8%87%AA%E5%AE%9A%E4%B9%89%E8%AF%BB%E5%8F%96%E6%95%B0%E6%8D%AE%E5%BA%93%E9%85%8D%E7%BD%AE/https://github.com/PhraseApp-Blog/spring-boot-db-messageresourcehttps://medium.com/techcret/database-aware-i18n-messages-springboot-5715063094ef
符号说明不方便书写的符号使用转义符 "\" 来取消特殊语义例如^表示字符串的开始\^则表示匹配^这个符号。通常大写表示反义词例如\d表示匹配数字,\D表示匹配非数字常用匹配字符符号说明.匹配除换行外的所有字符\w匹配所有字母数字,等同于 [a-zA-Z0-9_]\d匹配数字: [0-9]\s匹配所有空格字符,等同于: [\t\n\f\r\p{Z}]\f匹配一个换页符\n匹配一个换行符\r匹配一个回车符\t匹配一个制表符\v匹配一个垂直制表符\p匹配 CR/LF(等同于 \r\n),用来匹配 DOS 行终止符字符,数字,空格字符有反义匹配即\W,\D,\S限定符,量词符号说明*出现0次或多次+出现1次或多次?出现0次或1次{n}出现n次{n,m}出现n次至m次,可不写n或m表示至少n次或至多m次贪婪匹配默认为贪恋匹配,即默认尽可能多的匹配"/(.*at)/" => The fat cat sat on the mat.惰性匹配在量词后加上?可将匹配模式修改为惰性匹配,尽可能少的匹配"/(.*?at)/" => The fat cat sat on the mat.定位符定位符仅匹配位置并不匹配指定字符符号说明^开始位置$结束位置\b匹配单词的开始或结束\B匹配非单词的开始或结束范围符号说明|匹配左侧或者右侧, x|y 表示匹配x或者y,左右可以是分组[]括号内为匹配范围,[avd],表示avd三个字母都可匹配[a-z]表示匹配连续的范围a到z[^a-z]表示匹配a到z之外的任意字符分组符号说明()括号内为子表达式,子表达式可被引用\nn为1-9之间的数字,在正则表达式中来引用表达式中的分组,例如(a)\1,等价于aa分组之后的表达式可在文本编辑器中使用$n来引用其他正则表达式修饰符不同语言对正则表达式有不同的修饰符例如忽略大小写,多行匹配等,js是在/表达式/修饰符 中配置。具体需要看使用语言中的实现。 js中正则表达式的标记,可组合使用,在正则表达式最后加上符号符号说明g全局匹配i不区分大小写m多行搜索s允许.匹配换行符默认正则会返回第一个匹配的结果,加上全局匹配后会返回所有匹配的结果"/.(at)/" => The fat cat sat on the mat."/.(at)/g" => The fat cat sat on the mat.特殊字符匹配符号说明\xXX编号在 0 ~ 255 范围的字符,比如:点击测试 空格可以使用 "\x20" 表示\uXXXX任何字符可以使用 "\u" 再加上其编号的4位十六进制数表示,比如:点击测试 "\u4E2D"匹配规则,环视符号说明(?:pattern)捕获但不单独获取结果(?=pattern)肯定查询,仅当匹配字符串符合后缀匹配内容才判定为匹配,例如Windows(?=95|98|NT|2000)(?<=pattern)效果同上但放在字符串前,匹配前缀,例如 (?<=95|98|NT|2000)Windows(?!pattern)否定查询,仅当匹配字符串不符合匹配后缀才判定为匹配(?<!pattern)效果同上但放在字符串之前,匹配前缀,例如 (?<!95|98|NT|2000)Windows资料将正则表达式图形化,方便理解https://regexper.com/https://jex.im/regulex/2.在线测试正则https://regex101.com/r/AyAdgJ/1https://regexr.com/参考资料https://blog.csdn.net/hello_word2/article/details/84890548https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressionshttps://www.runoob.com/regexp/regexp-tutorial.htmlhttp://www.regexlab.com/zh/regref.htmhttps://xie.infoq.cn/article/dcc7103126233028945c67c43https://github.com/ziishaned/learn-regex/blob/master/translations/README-cn.md
dynamic-datasource-spring-boot-starter简介dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器,支持通过注解切换数据源,和mybatis-plus集成等,可查看代码中samples这个子项目。配置pom中增加依赖<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.1.1</version> </dependency>application配置文件中配置主库spring.datasource.dynamic.datasource.master.url=jdbc:mysql://xx.xx.xx.xx:3307/dynamic spring.datasource.dynamic.datasource.master.username=root spring.datasource.dynamic.datasource.master.password=root spring.datasource.dynamic.datasource.master.driver-class-name=com.mysql.jdbc.Driver添加数据源//动态数据源 @Autowired protected DataSource dataSource; //数据源创建器 @Autowired protected DataSourceCreator dataSourceCreator; //创建数据源 public void createNewDataSource(){ DynamicRoutingDataSource drds = (DynamicRoutingDataSource) dataSource; DataSourceProperty dsp = new DataSourceProperty(); dsp.setPoolName(dbname);//链接池名称 dsp.setUrl(dburl);//数据库连接 dsp.setUsername(username);//用户名 dsp.setPassword(password);//密码 dsp.setDriverClassName(driverClassName);//驱动 //创建数据源并添加到系统中管理 DataSource dataSource = dataSourceCreator.createDataSource(dsp); drds.addDataSource(dbname, dataSource); } //手动切换数据源 public void demo(){ DynamicDataSourceContextHolder.push(dbname);//数据源名称 try{ // your code 需注意使用后一定要使用poll清空数据源, }catch(Exception e){ }finnally{ DynamicDataSourceContextHolder.poll(); } }
Spring Security官方文档 Spring Security和Shiro都是安全框架,其中包含了很多内容本文主要记录一下自己理解的授权认证部分希望能表达的尽量简洁和完整,欢迎交流。 formlogin主体流程 其中认证相关Filter负责构建Token实体(未认证),并交给AuthenticationProvider进行验证Token并重新构建Token实体(已认证)。具体可参见org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter内源码。 还包含其它认证流程如BasicAuthenticationFilter,DigestAuthenticationFilter等,官方给的formlogin就是UsernamePasswordAuthenticationFilter相关的一整套流程,有兴趣的话可以看下formlogin这个方法对应的源码,其中包含了很多内容这里主要记录的是认证授权。 配置说明 @Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { /** * 配置认证相关信息自定义AuthenticationProvider,UserDetailsService等 * @param auth * @return void * @author mjm * @date 2020/1/20 16:08 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); } /** * 整体设置,异常处理,需忽略的url等 * @param web * @return void * @author mjm * @date 2019/12/30 14:07 */ @Override public void configure(WebSecurity web) { web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**") .antMatchers("/css/**", "/fonts/**", "/img/**", "/js/**", "/plugins/**"); } /** * 请求过程相关配置,formlogin,权限验证,cors,自定义过滤器等 * @author mjm * @date 2020/1/20 16:13 * @param http * @return void */ @Override protected void configure(HttpSecurity http) throws Exception { http.cors().disable().csrf().disable().headers().frameOptions().disable(); http.formLogin(); http.authorizeRequests().antMatchers("/**").access("@sysAuthorize.check(authentication,request)"); } } HttpSecurity说明: 1. 匹配url的顺序是从上至下 access是自定义权健部分具体可参见官网中"Referring to Beans in Web Security Expressions"这一段,其中能传递的参数只有authentication(认证信息),request 自定义过滤器通过http.addFilterXXXX来添加可以指定过滤器的顺序 配置了formlogin SpringSecurity会通过默认配置实现一整套的认证流程,包含页面,但需要实现UserDetailsService 这里列举的是我认为比较重要的三个配置,WebSecurityConfigurerAdapter中包含很多其他的配置具体具体可参见源码 其它 1.自定义认证过程如何处理 继承AbstractAuthenticationProcessingFilter public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { // token = xxx return this.getAuthenticationManager().authenticate(token); }
Pac4j 简介 Pac4j与Shiro,Spring Security一样都是权限框架,并且提供了OAuth - SAML - CAS - OpenID Connect - HTTP - OpenID - Google App Engine - Kerberos (SPNEGO) 的认证集成。且可以和shiro,security等权限框架集成。 Pac4j CAS认证流程 代码 关键部分 说明: pac4j-cas与shiro的集成是通过过滤器完成cas认证,提供相应的Pac4jRealm来与shiro集成。代码过多就不一一列出了,详细的请下载附件,附件中代码屏蔽了公司相关代码。自身项目需要保持CAS与非CAS并存所以把CAS登录固定到指定路径了。 POM <!--cas认证 --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-cas</artifactId> <version>3.8.3</version> </dependency> <!-- pac4j与shiro集成--> <dependency> <groupId>io.buji</groupId> <artifactId>buji-pac4j</artifactId> <version>4.1.1</version> </dependency> JAVA配置 //Pac4jConfig.java 配置中 @Bean public CasConfiguration casConfig() { final CasConfiguration configuration = new CasConfiguration(); //CAS server登录地址 configuration.setLoginUrl(casServerUrl + "/login"); configuration.setAcceptAnyProxy(true); configuration.setPrefixUrl(casServerUrl + "/"); //监控CAS服务端登出,登出后销毁本地session实现双向登出 DefaultLogoutHandler logoutHandler = new DefaultLogoutHandler(); logoutHandler.setDestroySession(true); configuration.setLogoutHandler(logoutHandler); return configuration; } //ShiroConfig.java 中 //shiro 过滤器配置中增加SecurityFilter,CallbackFilter ,LogoutFilter @Bean("shiroFilter") public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //获取filters Map<String, Filter> filters = shiroFilterFactoryBean.getFilters(); filters.put("authc", new MySystemFilter()); // cas 资源认证拦截器 SecurityFilter securityFilter = new SecurityFilter(); securityFilter.setConfig(exPac4jConfig); securityFilter.setClients(clientName); filters.put("securityFilter", securityFilter); //cas 认证后回调拦截器 CallbackFilter callbackFilter = new CallbackFilter(); callbackFilter.setConfig(exPac4jConfig); filters.put("callbackFilter", callbackFilter); shiroFilterFactoryBean.setFilters(filters); // 本地登出同步登出CAS服务器 LogoutFilter pac4jCentralLogout = new LogoutFilter(); pac4jCentralLogout.setConfig(exPac4jConfig); pac4jCentralLogout.setCentralLogout(true); pac4jCentralLogout.setLocalLogout(true); filters.put("pac4jCentralLogout", pac4jCentralLogout); //拦截器. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/pac4jCentralLogout", "pac4jCentralLogout"); filterChainDefinitionMap.put("/cas", "securityFilter"); filterChainDefinitionMap.put("/callback", "callbackFilter"); filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("index"); shiroFilterFactoryBean.setUnauthorizedUrl("/error/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setAuthenticator(exModularRealmAuthenticator()); List<Realm> realms = new ArrayList<>(); realms.add(exSystemRealm()); // casRealm继承Pac4jRealm 与shiro的Realm使用方法相同 realms.add(casRealm); securityManager.setRealms(realms); securityManager.setCacheManager(redisCacheManager()); //增加pac4jSubjectFactory securityManager.setSubjectFactory(pac4jSubjectFactory); securityManager.setRememberMeManager(cookieRememberMeManager()); securityManager.setSessionManager(sessionManager()); return securityManager; } 问题 默认配置不支持CAS登出本地项目退出 重写ShiroSessionStore见ExShiroSessionStore.java 附件:链接: https://pan.baidu.com/s/1E-6uTYpOFn2ldAxd_k0XvQ 提取码: 8nhx 参考资料 https://www.cnblogs.com/suiyueqiannian/p/9359597.html http://www.pac4j.org/docs/index.html https://github.com/bujiio/buji-pac4j https://github.com/gkaigk1987/shiro-pac4j-cas-demo
LogMX logmx是一款商业日志查看的工具,可以监控动态日志等。但是提供了评估版可以免费使用。有以下功能 打开/监控本地及远程日志文件 支持log4j/logback,SysLog等格式的日志文件 日志查看筛选等 日志格式匹配 点击下图中打开log4j/Logback匹配介绍,找到对应关系填充即可 我用的是springboot默认的格式,匹配语法:%d{yyyy-MM-dd HH:mm:ss.SSS}%-5level %cn %mx{x}[%thread] %logger : %m%n %d{yyyy-MM-dd HH:mm:ss.SSS}:日期格式 %-5level: 日志级别 %cn:对应springboot中PID %mx{x}:对应---,如果直接使用---下次打开时日志格式就会丢失,所以使用一个占位符 [%thread]:对应线程名称 %logger :对应日志输出路径 %m : 日志消息 %n :换行符 配置好之后打开日志文件效果如下 使用过程中碰到的问题 中文乱码 设置字体为中文字体
配置 pom Spring Boot 使用的是2.1.6.RELEASE,依赖中增加如下配置 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> application.properties 调度器可以和springboot公用数据源 #使用数据库固化调度信息 spring.quartz.job-store-type=jdbc #调度器名称 spring.quartz.scheduler-name=MyScheduler #不重新创建数据表 spring.quartz.jdbc.initialize-schema=never #线程数量 spring.quartz.properties.org.quartz.threadPool.threadCount = 50 #持久化实现 spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX #数据库方言StdJDBCDelegate spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate java代码 任务:实现Job接口即可 public interface Job { //context中包含当前任务关联的信息 //JobExecutionException 当任务执行失败时可以通过配置来控制是否继续执行等操作 void execute(JobExecutionContext context) throws JobExecutionException; } 调度器:在service中注入Scheduler即可,Scheduler是调度器整体管理包括暂停任务,更新任务,恢复任务等 需求以及解决方法 每个任务使用相同上下文即JobData继承Job的java类上增加@PersistJobDataAfterExecution,@DisallowConcurrentExecution注解,通常这两个注解配合使用 @PersistJobDataAfterExecution:在任务执行后固化JobData至数据库 @DisallowConcurrentExecution:避免同一个组的同一个任务并发执行以免JobData混乱 更新JobData至当前任务 //jobDetail 任务信息 JobDetail jobDetail = scheduler.getJobDetail(JobKey.jobKey(job.getName(), job.getGroup())); jobDetail.getJobDataMap().put("aaa", "bbb"); CronTrigger trigger = (CronTrigger) scheduler.getTrigger(TriggerKey.triggerKey(job.getName(), job.getGroup())); Set<Trigger> triggers = new HashSet<>(); triggers.add(trigger); //true 就是替换数据库中JobDataMap scheduler.scheduleJob(jobDetail,triggers,true); 任务控制 暂停任务:scheduler.pauseJob(jobKey) 恢复任务:scheduler.resumeJob(jobKey) 立即执行任务:scheduler.triggerJob(jobKey) 执行中的任务: List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs() //获取具体任务信息 JobDetail jobDetail = executingJob.getJobDetail(); 查询任务 //依据分组查询,如需其他查找查看api中实现了org.quartz.Matcher接口的类即可 GroupMatcher<JobKey> matcher = GroupMatcher.groupContains(groupKeyword); Set<JobKey> jobKeys = scheduler.getJobKeys(matcher); //通过JobKey获取调度器中具体任务以及相关信息 scheduler.getJobDetail(jobKey); 参考资料 https://eelve.com/archives/springbootstarterquartzs http://www.quartz-scheduler.org/ https://www.w3cschool.cn/quartz_doc/
maven 依赖 hibernate新版本中去掉了sqlite的支持,如要使用需要导入jar包 <dependency> <groupId>com.zsoltfabok</groupId> <artifactId>sqlite-dialect</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> <version>3.20.0</version> </dependency> properties配置 # 配置方言com.zsoltfabok包中 spring.jpa.database-platform=org.hibernate.dialect.SQLiteDialect # 最好改成false,自己手动建表 spring.jpa.generate-ddl=false #spring.jpa.hibernate.ddl-auto 仅支持create-drop和create spring.datasource.driver-class-name=org.sqlite.JDBC
公众号端配置 1.模板消息需要提前申请,入口在添加功能插件中,通过审批之后在功能-模板消息。需要注意的是行业决定了模板可以选择的范围,行业可以改但是需要时间。 2.进入微信公众平台在设置菜单中找到公众号设置,进入后设置网页授权对应的域名 获取公众号openid过程 注意:*.2,3步为网页之间的跳转,需要先走微信认证服务器由微信服务器返回至网页,且不能携带参数,网页端通过参数中有没有code来判断是否完成了认证请求。*.第4步中解密code携带的用户信息需要再服务端完成,需要访问微信服务器,携带的参数有公众号appid,开发者密码,在“微信公众平台-开发-基本配置”中获取。 具体授权,解密链接地址及参数请参考官方文档 发送公众号模板消息 通过开发者密码和公众号appid获取token,可把token缓存起来避免频繁访问 按照模板参数发送请求。 这一环没啥可说的可参考官方文档中“发送模板消息”这一块 碰到的问题 问题: 授权页面不能带端口,但是实际项目在有端口的项目上解决方法: 在80端口工程下增加一个跳转页面跳转至其它端口下,页面代码如下 <script type="text/javascript" > var url = '……'; <!--这里就将页面重定向到新页面,同时带入原有参数--> var search = window.location.search; if (search){ if (url.indexOf("?") != -1){ url += "&"; }else{ url += "?"; } url += search.substr(1) + window.location.hash; } window.location.href= url; </script> 问题: 原生js代码关闭公众号打开的网页无效解决方法: 调用微信api的方法 WeixinJSBridge.call('closeWindow'); 其它补充 微信除了微信公众平台之外还有个微信开放平台如微信公众号与微信小程序共同绑定一个微信开放平台那么可以从code中解密出一个unionid 公众号和小程序的openid是独立的但是unionid是一样的。通过unionid可以省去公众号绑定系统账号这一环。 参考资料 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.htmlhttps://blog.csdn.net/Rafireman/article/details/76804541
vuepress 官网 vuepress 是使用vue驱动的静态网站生成器,与Docsify 不一样的是编译它后是静态网页可以直接拷贝到其它项目中使用。 目录结构 docs:用于存储markdown文件 docs.vuepress:用于存储vuepress配置文件,样式,以及公共文件 docsdemo:非固定名称,存储markdown的二级目录,可以有多个 docsimages:非固定名称,存储markdown中使用的图片,不能放到二级目录中否则编译中会被忽略 docsREADME.md : 文档首页,加入部分特殊语法的markdownREADME特殊部分如下,其它的随意。 actionLink:点击按钮后进入的页面。 --- home: true heroImage: actionText: 快速开始 → actionLink: /demo/test1/ features: - title:title1 details: details1 - title: title2 details: details2 - title: title3 details: details3 footer: MIT Licensed | Copyright © 2019-d --- 配置说明 重要配置说明: dest :输出的目录地址可以使用相对路径 base:部署站点的基础路径,如果你想让你的网站部署到一个子路径下,你将需要设置它。如 GitHub pages,如果你想将你的网站部署到 https://foo.github.io/bar/,那么 base 应该被设置成 "/bar/",它的值应当总是以斜杠开始,并以斜杠结束。 plugins: 使用的插件,需要先安装 themeConfig-sidebar:侧边栏目录,使用/结束时默认找此目录下的README.md 文件,否则使用名称+.md为结尾,编译后会使用文件中一级目录作为侧边栏目录名称 module.exports = { title: 'test title', description: 'test description', head: [ ['link', { rel: 'icon', href: '/favicon.ico' }] ], dest: '../static/helpDoc', // 设置输出目录 // 注入到当前页面的 HTML <head> 中的标签 base: '/Demo/helpDoc/', markdown: { lineNumbers: true // 代码块显示行号 }, plugins: [ 'flowchart' ], themeConfig: { lastUpdated: 'Last Updated', // 文档更新时间 // 侧边栏配置 sidebar: [{ "children": [ "/demo/test1", "/demo/test2" ], "collapsable": true, "title": "demo" } ] } } 自定义样式 index.styl :设置自定义样式覆盖原有样式palette.styl :全局样式设置,主要是设置颜色 碰到的问题 图片不能放在二级目录中,否则构建的时候会忽略。
通过@Profile+spring.profiles.active spring.profiles.active:官方解释是激活不同环境下的配置文件,但是实际测试发现没有对应的配置文件也是可以正常执行的。那就可以把这个key当作一个参数来使用@Profile:spring.profiles.active中激活某配置则在spring中托管这个bean,配合@Component,@Service、@Controller、@Repository等使用 @Component @Profile("xx") public class XxxTest extends BaseTest { public void test(){ System.out.println("in XxxTest "); } } @Component @Profile("yy") public class YyyTest extends BaseTest { public void test(){ System.out.println("in YyyTest "); } } @Service public class MyService { @Autowired private BaseTest test; public void printConsole(){ test.test(); } } //配置文件激活某个环境则test就会注入哪个bean spring.profiles.active=xx 通过@Configuration+@ConditionalOnProperty @Configuration:相当于原有的spring.xml,用于配置spring@ConditionalOnProperty:依据激活的配置文件中的某个值判断是否托管某个bean,org.springframework.boot.autoconfigure.condition包中包含很多种注解,可以视情况选择 @Configuration public static class ContextConfig { @Autowired private XxxTest xxTest; @Autowired private YyyTest yyTest; @Bean @ConditionalOnProperty(value = "myTest",havingValue = "xx") public BaseTest xxxTest() { return xxTest; } @Bean @ConditionalOnProperty(value = "myTest",havingValue = "yy") public BaseTest yyyTest() { return yyTest; } //配置文件中控制激活哪个bean myTest=xx } 参考资料 https://blog.csdn.net/wild46cat/article/details/71189858https://www.javacodegeeks.com/2013/10/spring-4-conditional.html
核心类简介 xxxToken:用户凭证xxxFilter:生产token,设置登录成功,登录失败处理方法,判断是否登录连接等xxxRealm:依据配置的支持Token来认证用户信息,授权用户权限 核心配置 Shrio整体配置:ShrioConfig.java @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, Filter> filters = shiroFilterFactoryBean.getFilters(); //将自定义 的FormAuthenticationFilter注入shiroFilter中 filters.put("authc", new AuthenticationFilter()); filters.put("wechat",new ExWechatAppFilter()); shiroFilterFactoryBean.setFilters(filters); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); ... //建立url和filter之间的关系 filterChainDefinitionMap.put("/wechat/**","wechat"); filterChainDefinitionMap.put("/**", "authc"); ... shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setAuthenticator(exModularRealmAuthenticator()); List<Realm> realms = new ArrayList<>(); //设置多Realm realms.add(systemRealm()); realms.add(wechatAppRealm()); securityManager.setRealms(realms); securityManager.setCacheManager(ehCacheManager()); securityManager.setRememberMeManager(cookieRememberMeManager()); return securityManager; } //重要!!定义token与Realm关系,设置认证策略 public MyModularRealmAuthenticator myModularRealmAuthenticator(){ MyModularRealmAuthenticator authenticator = new MyModularRealmAuthenticator(); FirstSuccessfulStrategy strategy = new FirstSuccessfulStrategy(); authenticator.setAuthenticationStrategy(strategy); return authenticator; } @Bean public SystemRealm systemRealm() { SystemRealm systemRealm = new SystemRealm(); systemRealm.setAuthorizationCachingEnabled(true); systemRealm.setAuthorizationCacheName("authorization"); systemRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return systemRealm; } @Bean public WechatAppRealm WechatAppRealm(){ WechatAppRealm wechatAppRealm = new WechatAppRealm(); wechatAppRealm.setAuthorizationCachingEnabled(false); return WechatAppRealm; } Realm,Token关联关系配置:MyModularRealmAuthenticator.java public class MyModularRealmAuthenticator extends ModularRealmAuthenticator { @Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); //依据Realm中配置的支持Token来进行过滤 List<Realm> realms = this.getRealms() .stream() .filter(realm -> realm.supports(authenticationToken)) .collect(Collectors.toList()); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.get(0), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } } } 认证授权配置:Realm.java public class SystemRealm extends AuthorizingRealm { ... @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //重要!!多realm每个都会执行授权相关信息,此处进行过滤 if(principals.fromRealm(getName()).isEmpty()){ return null; } //授权代码... return authorizationInfo; } /** * 主要是用来进行身份认证的 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //生产AuthenticationInfo代码... //校验的部分由配置的credentialsMatcher进行处理 return authenticationInfo; } /** * 扩展认证token * * @param authenticationToken * @return boolean * @author mjm * @date 2018/7/3 12:32 */ @Override public boolean supports(AuthenticationToken authenticationToken) { //设置此Realm支持的Token return authenticationToken != null && (authenticationToken instanceof UsernamePasswordToken ); } } 过滤器配置:AuthenticationFilter.java 基础的过滤器类型:官网中默认有很多已实现的过滤器,可依据需求扩展 public class AuthenticationFilter extends FormAuthenticationFilter { .... /** * 创建令牌 * * @param servletRequest ServletRequest * @param servletResponse ServletResponse * @return 令牌 */ @Override protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) { //依据request中不同的参数创建不同的token... return new xxxToken(...); } .... } 参考资料 http://shiro.apache.org/realm.html#Realm-Supporting%7B%7BAuthenticationTokens%7D%7D
简单使用 依赖 <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </dependency> <!--转换器log4j 转 logback--> <dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> </dependency> <!--过滤日志用到--> <dependency> <groupId>org.codehaus.janino</groupId> <artifactId>janino</artifactId> </dependency> <!--日志邮件--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-email</artifactId> <version>1.5</version> </dependency> 配置 #输出日志文件地址 logging.file=E:/LOG/ex.info.log #日志输出级别 logging.level.root=info #指定org.springframework.web.servlet.DispatcherServlet类日志级别为DEBUG logging.level.org.springframework.web.servlet.DispatcherServlet=debug 开启logback独立配置 #指定logback配置文件路径,如果配置文件在根目录可自动匹配logback-spring.xml,logback.xml logging.config=classpath:config/logback.xml logback配置 核心节点简介 节点 说明 root 控制项目整体输出日志级别,子节点为appender logger 控制指定包或者类的输出级别,和引用的appender,子节点为appender appender 控制日志输出形式,格式 其它配置 节点 说明 contextName 上下文可在输出的地方用%contextName来引用 property 定义属性可在节点中通过${propertyName}来使用 conversionRule 自定义格式转换符,spring已自定义了彩色日志,异常日志格式直接引用即可 root,logger root可以理解为一个特定的logger用来控制整个项目的日志输出级别。 logger下可配置0个或多个appender-ref,用于指定选择的日志输出方式 属性名称 说明 name 包名或类名完整路径 level 指定输出级别 addtivity 是否向上级传递信息,默认为true appender appender控制日志输出到哪里,用什么形式输出,,后续的例子也可以参考,例子中包含输出到控制台,输出到文件,输出到邮箱。更详细的内容请查看官网文档 属性名称 说明 name appender名称,在logger或root中通过appender-ref来引用 class 处理日志的类 日志信息 日志内容 appender中encoder标签用于控制输出什么内容输出的内容包含很多参数下面列举一些我用到的参数,详细内容请查看官网文档 名称 别名 说明 %date{format} %d 日期,format为格式化方式 %thread %t 线程名 %level %le,%p 日志级别 %logger{length} %lo,%c 打印日志的类,length为输出的长度,如输出类名称过长则会进行缩写 %method %M 打印日志的方法,影响性能谨慎使用 %caller 调用方法的堆栈,有其它参数请参见官网文档 %message %msg,%m 日志内容 %xException %xEx,%xThrowable 异常堆栈,包含包名 %n 当前系统中换行符 日志格式 例子:%-20.30logger 说明 -:表示居左,默认居右 20:表示最小长度,不够的补全空格 30:表示最大长度,超出的自动裁剪,logger会简写包名 过滤器 执行一个过滤器会有返回个枚举值,即DENY,NEUTRAL,ACCEPT其中之一。 **DENY:**日志将立即被抛弃不再经过其他过滤器 **NEUTRAL:**有序列表里的下个过滤器过接着处理日志 **ACCEPT:**日志会被立即处理,不再经过剩余过滤器 下面简单介绍一下我使用到的两个过滤器,其它请参考详细文档 日志内容过滤器 ch.qos.logback.core.filter.EvaluatorFilter:通过指定条件来控制是否输出,其中expression为java代码可直接使用java中方法。 <appender name="REQUEST_CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <filter class="ch.qos.logback.core.filter.EvaluatorFilter"> <evaluator> <!-- 默认为 ch.qos.logback.classic.boolex.JaninoEventEvaluator || message.startsWith("Returning handler method")--> <expression> <![CDATA[ return (!message.contains(".") && message.startsWith("Looking up handler method")) ; ]]> </expression> </evaluator> <OnMatch>ACCEPT</OnMatch> <OnMismatch>DENY</OnMismatch> </filter> </appender> 日志级别过滤器 ch.qos.logback.classic.filter.LevelFilter:通过指定日志级别来过滤 <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> logback配置例子 <?xml version="1.0" encoding="UTF-8"?> <!-- scan: 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。 scanPeriod: 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 debug: 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 --> <configuration > <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径--> <property name="log.path.root" value="E:/log/" /> <!--编码--> <property name="log.charset" value="UTF-8" /> <!-- 控制台财色输出 --> <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" /> <!-- spring 美化异常输出 --> <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" /> <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" /> <!--日志格式 --> <property name="log.pattern.file" value="%d{yyyy-MM-dd HH:mm:ss.SSS}%-5level ${PID:- } --- [%thread] %logger : %msg%n%xException" /> <property name="log.pattern.error.file" value="%d{yyyy-MM-dd HH:mm:ss.SSS}%-5level ${PID:- } --- [%thread] %logger %method : %msg%n%caller{1..3}%xException" /> <!-- 彩色日志格式 --> <property name="log.pattern.console" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}) %clr(%-5level) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%-10.10thread]){faint} %clr(%-40.40logger{39}){blue} %clr(:){faint} %m%n%xException" /> <!-- 控制台输出--> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder charset="${log.charset}"> <pattern>${log.pattern.console}</pattern> </encoder> </appender> <!-- info 日志文件 --> <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path.root}ex.info.log</file> <append>true</append> <encoder charset="${log.charset}"> <pattern>${log.pattern.file}</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 滚动策略 大小 策略 --> <fileNamePattern>${log.path.root}/%d{yyyy-MM-dd}exinfo-%i.log</fileNamePattern> <maxFileSize>10MB</maxFileSize><!-- 单个日志大小 --> <maxHistory>30</maxHistory><!--保留的归档文件的最大数量 --> </rollingPolicy> </appender> <!-- warn 日志文件 --> <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path.root}ex.error.log</file> <append>true</append> <encoder charset="${log.charset}"> <pattern>${log.pattern.error.file}</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 滚动策略 大小 策略 --> <fileNamePattern>${log.path.root}/%d{yyyy-MM-dd}exerror-%i.log</fileNamePattern> <maxFileSize>10MB</maxFileSize><!-- 单个日志大小 --> <maxHistory>30</maxHistory><!--保留的归档文件的最大数量 --> <totalSizeCap>${total.size.cap}</totalSizeCap> </rollingPolicy> </appender> <!-- 请求日志过滤--> <appender name="REQUEST_CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <filter class="ch.qos.logback.core.filter.EvaluatorFilter"> <evaluator> <!-- 默认为 ch.qos.logback.classic.boolex.JaninoEventEvaluator || message.startsWith("Returning handler method")--> <expression> <![CDATA[ return (!message.contains(".") && message.startsWith("Looking up handler method")) ; ]]> </expression> </evaluator> <OnMatch>ACCEPT</OnMatch> <OnMismatch>DENY</OnMismatch> </filter> <encoder charset="${log.charset}"> <pattern>${log.pattern.console}</pattern> </encoder> </appender> <appender name="REQUEST_FLOW_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path.root}ex.request.flow.log</file> <append>true</append> <encoder charset="${log.charset}"> <pattern>${log.pattern.file}</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 滚动策略 大小 策略 --> <fileNamePattern>${log.path.root}/%d{yyyy-MM-dd}ex.request.flow-%i.log</fileNamePattern> <maxFileSize>10MB</maxFileSize><!-- 单个日志大小 --> <maxHistory>30</maxHistory><!--保留的归档文件的最大数量 --> <totalSizeCap>${total.size.cap}</totalSizeCap> </rollingPolicy> </appender> <!-- ERROR邮件发送 --> <appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender"> <smtpHost>smtp.126.com</smtpHost> <smtpPort>25</smtpPort> <username>xx@mail.com</username> <password>password</password> <asynchronousSending>true</asynchronousSending> <SSL>true</SSL> <to>xx@mail.com</to> <from>xx@mail.com</from> <subject>%d{yyyy-MM-dd HH:mm:ss.SSS}错误日志</subject> <layout> <Pattern>${log.pattern.error.file}}</Pattern> </layout> <cyclicBufferTracker class="ch.qos.logback.core.spi.CyclicBufferTracker"> <!-- 缓冲的日志数量 --> <bufferSize>5</bufferSize> </cyclicBufferTracker> <!-- 这里采用等级过滤器 指定等级相符才发送 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <!--系统警告日志打印到单独日志文件--> <logger name="com.bmw.frame" level="ERROR" additivity="false"> <appender-ref ref="CONSOLE" /> <appender-ref ref="ERROR_FILE" /> </logger> <!--系统警告日志打印到单独日志文件--> <logger name="com.example.test" level="TRACE" additivity="false"> <appender-ref ref="CONSOLE" /> <appender-ref ref="INFO_FILE" /> </logger> <!-- 1. 输出SQL 到控制台和文件,生产环境视情况打开--> <logger name="org.hibernate.SQL" level="DEBUG" additivity="false" > <!--<appender-ref ref="CONSOLE" />--> <appender-ref ref="INFO_FILE" /> </logger> <!-- 2. 输出SQL 的参数到控制台和文件,生产环境视情况打开--> <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false" > <!--<appender-ref ref="CONSOLE" />--> <appender-ref ref="INFO_FILE" /> </logger> <!-- 3 输出请求路径到文件 org.springframework.web.servlet.DispatcherServlet 没有请求执行的方法所以使用当前日志 --> <logger name="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" level="DEBUG" additivity="false" > <appender-ref ref="REQUEST_CONSOLE" /> <appender-ref ref="INFO_FILE" /> </logger> <!-- 4 输出请求耗时流水到文件,生产环境视情况打开--> --> <logger name="org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext" level="TRACE" additivity="false" > <appender-ref ref="REQUEST_FLOW_FILE" /> </logger> <!-- 日志输出级别 --> <root level="INFO"> <appender-ref ref="CONSOLE" /> <appender-ref ref="INFO_FILE" /> </root> </configuration> 参考资料 https://aub.iteye.com/blog/1101222 https://logback.qos.ch/manual/layouts.html
环境安装 nodejs安装与配置 nodejs官网,目前nodejs稳定版: 8.12.0 npm 6.4.1 安装完成之后可修改npm全局包的存放位置,仓库的位置 //全局包目录,就在node安装目录新建了个nodejs文件夹存放 npm config set prefix D:/nodejs/node_global/ //全局包缓存目录,就在node安装目录新建了个nodejs文件夹存放 npm config set cache D:/nodejs/node_cache/ //设置仓库为阿里镜像, npm config set registry https://registry.npm.taobao.org/ npm设置完之后需在环境变量中将设置的全局包目录添加到path中 creat-react-app 安装 这个是官方出的react的脚手架,官方推荐使用yarn来进行包管理 ,这里使用的是npm有兴趣的可以去看看。 -g表示全局安装 npm install -g create-react-app 安装完成之后就可以使用creat-react-app 项目名称来创建项目了,项目名称不能包含大写字母。 vscode安装 官网,安装没啥好说的照着指引一步步做就可以了,安装完成之后会询问是否增加环境变量,增加重启机器后可以使用 //以指定目录为工作空间打开一个窗口,当前目录为"." code 目录 针对react使用官方有一个指引,在这里就不赘述了。 其他 碰到的问题 使用create-react-app创建的项目,build的页面为空白页 // package.json 文件增加配置 "homepage": ".", VSCODE中代码提醒的快捷键设置 在[File-Preference-Keyboard Shortcusts]打开的页面中搜索 editor.action.triggerSuggest 换成 自己顺手的快捷键 参考资料 https://npm.taobao.org/ https://code.visualstudio.com/docs/nodejs/reactjs-tutorial https://www.jianshu.com/p/ec7c2bab16cc 来自为知笔记(Wiz)
N+1查询 使用JpaSpecificationExecutor来查询 在Specification.toPredicate 方法中使用fetch方法,写法如下,使用之后查询会关联查询,但是对于集合实体这种属性会产生错误数据,不建议集合属性使用这种方式查询。 public Predicate toPredicate(Root<实体> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) { root.fetch("xxx"); ... List<Predicate> predicates = new ArrayList<>(); return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()])); } 分页查询则需要增加一个判断,因为分页查询会先查一下count public Predicate toPredicate(Root<实体> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) { if(criteriaQuery.getResultType().equals(实体.class)){ root.fetch("xxx"); } ... List<Predicate> predicates = new ArrayList<>(); return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()])); } 使用@Query查询 @Query(value = "select d from Document d join fetch e.filetype ") List<User> findDocuments(); 黑科技:fetch all properties 积极查询所有属性,但是集合属性不会自动feach @Query(value = "select d from Document d fetch all properties") List<User> findDocuments(); 序列化实体导致的N+1查询 解决方法 对于json序列化最好的方式是使用@JsonIgnore来忽略集合属性,fetch实体属性。 在service中将实体转换为vo 参考资料 https://yq.aliyun.com/articles/2378 https://www.cnblogs.com/lcchuguo/p/5327738.html http://www.java2s.com/Tutorials/Java/JPA/4700__JPA_Query_Join_Fetch.htm https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/queryhql.html 来自为知笔记(Wiz)
简介 WebSocket:是一种网络通信协议,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息详情 sockjs-client:js库,如果浏览器不支持 WebSocket,该库可以模拟对 WebSocket 的支持github STOMP:简单(流)文本定向消息协议 介绍 stomp-websocket:js库,提供一个基于STOMP客户端的WebSocket gihub CODE 已下代码在demo中都有,但是有的为了博客效果没有简化。 Maven 增加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> SpringBoot 配置 注意事项 重写DefaultHandshakeHandler的determineUser方法来自己实现生成用户频道名称,如使用的是spring Security则可忽略此条 enableSimpleBroker:设置客户端接收消息的前缀 setUserDestinationPrefix:指定用户频道的前缀,这个前缀必须在enableSimpleBroker中设置过 @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) { //设置端点,前端通过 /ContextPath/端点 进行连接 stompEndpointRegistry.addEndpoint("/any-socket").addInterceptors(new HandshakeInterceptor() { /** * 握手前拦截,往attributes存储用户信息,后续用户频道使用 * @param request * @param response * @param wsHandler * @param attributes * @return * @throws Exception */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { boolean result = false; HttpSession session =getSession(request); if (session != null){ User user =(User)getSession(request).getAttribute("user"); if(user != null){ attributes.put("user",user); result = true; } } return result; } @Nullable private HttpSession getSession(ServerHttpRequest request) { if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request; return serverRequest.getServletRequest().getSession(); } return null; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {} }).setHandshakeHandler(new DefaultHandshakeHandler() { /** * 指定握手主体生成规则,后续接收用户消息时会使用,默认用户频道为/UserDestinationPrefix/{Principal.getName}/频道 * @param request * @param wsHandler * @param attributes * @return */ @Nullable @Override protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) { return new UserPrincipal((User) attributes.get("user")); } //支持SockJS }).withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry messageBrokerRegistry) { messageBrokerRegistry.setApplicationDestinationPrefixes("/app"); // 客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息 messageBrokerRegistry.enableSimpleBroker("/queue", "/topic"); //指定用户频道前缀,默认为user可修改 messageBrokerRegistry.setUserDestinationPrefix("/queue"); } @Override public void configureWebSocketTransport(final WebSocketTransportRegistration registration) { registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() { @Override public WebSocketHandler decorate(final WebSocketHandler handler) { return new WebSocketHandlerDecorator(handler) { @Override public void afterConnectionEstablished(final WebSocketSession session) throws Exception { String username = session.getPrincipal().getName(); //上线相关操作 super.afterConnectionEstablished(session); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { String username = session.getPrincipal().getName(); //离线相关操作 super.afterConnectionClosed(session, closeStatus); } }; } }); } } 服务端代码 ** 说明 ** @MessageMapping("/sendMsg"),对应前端发送消息时调用的路径,访问路径为/ApplicationDestinationPrefixes/sendMsg,此时已与ContextPath无关。 convertAndSendToUser:向指定用户发送消息,对应设置中的determineUser,和指定的用户频道前缀,最终发送的路径:/用户频道前缀/Principal.getName/后缀 convertAndSend:向指定频道发送消息,可以使用@SendTo代替 @SendToUser:向请求的用户对应的用户频道发送消息,与convertAndSendToUser不能互换 @MessageMapping("/sendMsg") public void sendMsg(Principal principal, String message) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss"); Message msg = JsonUtils.toObject(message, Message.class); try { msg.setSendTime(sdf.format(new Date())); } catch (Exception e) { } if (!"TO_ALL".equals(msg.getReceiver())) { template.convertAndSendToUser(msg.getReceiver(), "/chat", JsonUtils.toJson(msg)); } else { template.convertAndSend("/notice", JsonUtils.toJson(msg)); } } 前端代码 function connect() { //连接端点,此时需加上项目路径 var socket = new SockJS(_baseUrl+'any-socket'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { //监听/topic/notice这个频道的消息 stompClient.subscribe('/topic/notice', function (message) { showMessage(JSON.parse(message.body)); }); //监听当前登录用户这个频道的消息,对应服务端convertAndSendToUser stompClient.subscribe("/queue/"+_username+"/chat", function (message) { showMessage(JSON.parse(message.body)); }); }); } $("#send").click(function () { debugger; var msg = { "username":_username ,"avatar":_avatar ,"content":$("#message").val() ,"receiver":target };; //向MessageMapping对应路径发送消息 stompClient.send("/app/sendMsg", {}, JSON.stringify(msg)); $("#message").val(""); }); Demo demo参考了这个博客,去掉了Spring Security的部分,修改了一对一消息发送的规则,写的比较简陋,第一个用户登录的时候会报错大家将就看看就行。第二个用户登录后刷新第一个登陆用户的页面会加载用户,就可以点对点的聊天了。 https://gitee.com/MeiJM/stompDemo 参考资料 https://www.jianshu.com/p/4ef5004a1c81 https://www.jianshu.com/p/0f498adb3820 https://spring.io/guides/gs/messaging-stomp-websocket/ https://www.callicoder.com/spring-boot-websocket-chat-example/ http://www.ruanyifeng.com/blog/2017/05/websocket.html https://github.com/spring-guides/gs-messaging-stomp-websocket 来自为知笔记(Wiz)
JpaSpecificationExecutor简介 spring data jpa中负责jpa查询的接口,封装了常用的基于对象查询的各种方法,与第一篇中介绍的几种查询方式相比最大优势是是可以动态指定查询条件,但是查询结果目测只能以实体进行封装。 接口说明 JpaSpecificationExecutor接口说明 接口名称 说明 Optional findOne(@Nullable Specification spec); 查询一个实体 List findAll(@Nullable Specification spec); 指定查询条件查询 Page findAll(@Nullable Specification spec, Pageable pageable); 分页查询 List findAll(@Nullable Specification spec, Sort sort); 排序查询 long count(@Nullable Specification spec); 查询总数 注意事项:通过上面的接口我们会发现一个很诡异的问题,就是分页查询和排序好像不能同时存在,其实Pageable是有多个实现的,除了通常只有关于分页的查询之外还有一个PageRequest,这个类可以支持排序和分页共存。使用方法为PageRequest.of(int page, int size, Sort sort); Specification接口说明 这个接口下包含一个接口用于构建查询条件 Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder); 还有4个静态方法,静态方法分别是where,not,and,or参数为Specification.当查询条件可复用时可专门构建一个产生查询条件的工厂,并在查询时使用静态方法进行连接。 JPA查询说明 参数说明: 参数Root:查询根,join ,fetch深度获取,当属性为集合时使用 参数CriteriaQuery:查询主体,可增加groupBy,distinct,having, 参数CriteriaBuilder:构建查询条件及一些基础函数 返回值Predicate:条件组合 本来想写一些具体的例子,发现一个大神整理的jpa api用法汇总,就不献丑了。 其他 spirng boot jpa中获取EntityManager,获取之后可以在service中使用原生jpa代码构建jpql @Autowired private JpaContext jpaContext; public EntityManager getEm(){ return jpaContext.getEntityManagerByManagedType(实体类.class); } 简化jpa语法的工具包,有兴趣可以研究下 QueryDSL介绍,官网,官方支持功能强大,包也大。。 jpa-spec介绍,github,大神写的工具包,包小,简化构建Predicate 方法。 参考资料 https://www.baeldung.com/rest-api-search-language-spring-data-specifications http://www.importnew.com/24514.html https://lufficc.com/blog/spring-boot-jpa-querydsl http://www.cnblogs.com/xingqi/p/3929386.html 来自为知笔记(Wiz)
快捷键 代码类 快捷键 说明 Alt+C 代码提示(自改原ctrl+shift+space) Ctrl+Alt+T 对选中代码增加包围代码,例如try-catch Alt+Insert 对类增加getter/setter,toString之类的类级基础方法 Shift+F6 可以重命名你的类、方法、变量等等,而且这个重命名等 Ctrl+Alt+L 代码格式化 Ctr + Shift + u 大小写转换 Ctrl+O 重写父类方法 Ctrl+W 选中代码文本 Ctrl-P 方法参数提示。 Ctrl+Alt+O 优化导入的类和包 Ctrl+/或Ctrl+Shift+/ 注释(//或者/**/) Ctrl+Alt+鼠标左键 或 Ctrl+Alt+F3 打开接口的实现类 Ctrl+Alt+t 包裹代码块,try,if之类 编辑类 快捷键 说明 Ctrl+D/Ctrl+Y 复制当前行/删除当前行 CTRL+SHIFT+UP/DOWN 移动行 Ctrl+Shift+Alt +鼠标左键可以一行一行的选 Alt+鼠标左键向下拉 按住鼠标滚轮向下拉 列编辑 Alt+L 最后一次编辑的地方原(Ctrl+Shift+Backspace) 文件查找 快捷键 说明 Ctrl+F12 查看类结构 Alt+F7 查找变量,方法的引用位置 Ctrl+E 查找最近编辑的文件,以及项目中的TODO等项目信息 Ctrl+N 查找类 Ctrl+Shift+N 按文件名称 查找文件 Ctrl+Shift+F7 高亮所有选中文本 Ctrl+Shift+F 查找文本 Ctrl+Shift+R 替换文本 Shift+Shift 查找所有文件 提示 快捷键 说明 Ctrl+Q View | Quick Documentation 视图 查看代码文档 Ctrl+P View | Parameter Info 视图 参数信息 Ctrl+B Navigate | Declaration 导航 查看变量的声明,再次点击可以查看变量的调用 自动补全 快捷键 说明 Alt+J 提示所有智能补全代码 sout system.out itar for循环 来自为知笔记(Wiz)
场景描述 使测试人员可依据开发提交的问题编号更精确的提取升级包 Jenkins项目配置 基础配置: 注意事项 输入的参数需增加双引号 源码管理: 构建:注意事项:-Dbugids=%bugids% -DbeginTime=%beginTime% -DendTime=%endTime%,这一段为传递到maven中的参数需和参数配置中名字保持一致, maven命令中需增加test命令 JAVA项目端配置 简介:使用org.testng来接收Jenkins传递的参数,在测试方法中执行java类来进行捞取提交日志,并复制提交文件到升级包的操作. pom.xml 在properties中增加提包所需的参数,需与Jenkins中参数名一致 使用maven-surefire-plugin来传递Jenkins中参数到java测试类中: java代码 在测试包中增加提包相关代码,并使用@Parameters注解来接收maven传递过来的参数 来自为知笔记(Wiz)
Jenkins端 项目端 重要部分说明 Jenkins端 1.基本配置同maven配置即可,注意事项: 参数不需要增加双引号 构建:使用ant进行构建,此处的ant需在系统设置中先配置好 项目端 使用ant调用java代码来获取svn提交记录,并产生小包和日志 新增build.xml 重要部分说明 申明执行环境 执行java代码: 2.1. 使用java 标签来执行指定路径java类的main方法,java标签中classpath为java类编译后所在路径 2.2. java标签中classpath 为class文件执行所需的运行环境 2.3. java标签中arg 为传递给main函数的参数,使用${}来获取Jenkins传递的参数 来自为知笔记(Wiz)
JNA JNA(Java Native Access )提供一组Java工具类用于在运行期动态访问系统本地库(native library:如Window的dll)而不需要编写任何Native/JNI代码。开发人员只要在一个Java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射。 优点:JNA可以让你像调用一般java方法一样直接调用本地方法。就和直接执行本地方法差不多,而且调用本地方法还不用额外的其他处理或者配置什么的,也不需要多余的引用或者编码,使用很方便。 缺点:JNA是建立在JNI的基础之上的,所以效率会比JNI低。 关键代码 import com.sun.jna.Library; import com.sun.jna.Memory; import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.ptr.IntByReference; public class LYTest { public interface CLibrary extends Library { CLibrary INSTANCE = (CLibrary)Native.loadLibrary("ly_icparse",CLibrary.class); int Parse(String databuf,IntByReference ickh,IntByReference quantity,IntByReference fc,Pointer cid); int Build(int ickh, int quantity, int fc, String cid, Pointer databuf); } public static void main(String[] args) throws Exception { //用于接收输出的char* Pointer databuf = new Memory(512); CLibrary.INSTANCE.Build(20133058, 11, 3, "201013000285", databuf); byte[] byteArray = databuf.getByteArray(0, 512); String data = new String(byteArray,"UTF-8"); System.out.println("data:"+data); //构建读卡数据 String databufstr = "A2131091FFFF8115FFFF201013000285FFFFFFFFFFD27600000400FFFFFFFFFF"+data.substring(64,512); IntByReference ickh = new IntByReference(); IntByReference quantity = new IntByReference(); IntByReference fc = new IntByReference(); Pointer cid = new Memory(12); int result = CLibrary.INSTANCE.Parse(databufstr, ickh, quantity, fc, cid); String cidstr = new String(cid.getByteArray(0, 12),"UTF-8"); System.out.println("ickh:"+ickh.getValue()); System.out.println("quantity:"+quantity.getValue()); System.out.println("fc:"+fc.getValue()); System.out.println("cid:"+cidstr); System.out.println("result:"+result); } } 说明 常用的c于java参数对应关系 c参数 java参数 说明 int* IntByReference 出参,入参直接用int char* Pointer/Memory 出参,入参直接用String char*作为出参时需要知道对应的字符串长度在获得内容时使用。 来自为知笔记(Wiz)
<SCRIPT language=javascript> //var WshNetwork = new ActiveXObject("WScript.Network"); //ComputerName=WshNetwork.ComputerName+"/"+WshNetwork.UserName; //读注册表中的计算机名 var obj = new ActiveXObject("WScript.Shell"); var advance="HKEY_CURRENT_USER\\Software\\Microsoft\\Internet Explorer\\Main";//注册表关于高级设置路径 //IE浏览器——>工具——>Internet选项——>高级里的"禁止脚本调试(其他)" var str5=advance+"\\Disable Script Debugger"; alert(obj.RegRead(str5)); // obj.RegRead(str5); 读 // obj.RegWrite(str5,"yes"); 写 // obj.RegDelete(str5) 删 </SCRIPT> 注意:使用\\来切分路径 ,ActiveXObject对象为ie对象,该操作也只支持ie浏览器 详细说明 来自为知笔记(Wiz)
问题原因:因为oracle驱动需要官方授权,所以在pop.xml文件直接配置,无法下载成功. 解决方法:通过将驱动包安装到本地maven库,可以解决此问题。 1.在cmd中输入如下maven命令 mvn install:install-file -DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=11.2.0.1.0 -Dpackaging=jar -Dfile=ojdbc6.jar 其中-Dfile=ojdbc6.jar 为驱动包的相对路径 安装成功后就可以在pom.xml中使用 来自为知笔记(Wiz)
原文 TrueLicense是一个开源的证书管理引擎,官网 使用场景:当项目交付给客户之后用签名来保证客户不能随意使用项目 默认校验了开始结束时间,可扩展增加mac地址校验等。 其中还有ftp的校验没有尝试,本文详细介绍的是本地校验 license授权机制的原理: 生成密钥对,方法有很多。授权者保留私钥,使用私钥对包含授权信息(如使用截止日期,MAC地址等)的license进行数字签名。公钥给使用者(放在验证的代码中使用),用于验证license是否符合使用条件。 使用keytool生成密钥对 以下命令在dos命令行执行,注意当前执行目录,最后生成的密钥对即在该目录下: 首先要用KeyTool工具来生成私匙库:(-alias别名 –validity 3650表示10年有效) keytool -genkey -alias privatekey -keystore privateKeys.store -validity 3650然后把私匙库内的公匙导出到一个文件当中: keytool -export -alias privatekey -file certfile.cer -keystore privateKeys.store然后再把这个证书文件导入到公匙库: keytool -import -alias publiccert -file certfile.cer -keystore publicCerts.store 最后生成文件privateKeys.store、publicCerts.store拷贝出来备用。 使用LicenseCreate来生成需要的数字签名 配置文件说明: PRIVATEALIAS:对应生成的私匙库名称别名alias privatekey KEYPWD:该密码生成密钥对的密码,生成密钥对时录入的密码 STOREPWD:该密码是在使用keytool生成密钥对时设置的密钥库的访问密码 SUBJECT:生成的签名主题 licPath:生成的签名文件存放路径 priPath:使用的私匙库文件路径 剩下的为签名文件中的内容配置 其中consumerType和ConsumerAmount不明确如何使用 扩展签名文件中的自定义字段 //在CreateLicense#createLicenseContent方法中 // Extra中可以存储扩展的字段,校验时读取该信息即可 content.setExtra(new Object()); 使用LicenseVerify来验证签名 配置文件说明: PUBLICALIAS:对应生成的公匙库别名alias publiccert STOREPWD:该密码生成密钥对的密码,生成密钥对时录入的密码 SUBJECT:校验的签名主题 licPath:校验的签名文件存放路径 pubPath:使用的公匙库文件路径 扩展字段校验 修改cn.melina.license.VerifyLicense中verify方法 public boolean verify() { /************** 证书使用者端执行 ******************/ boolean result = true; LicenseManager licenseManager = LicenseManagerHolder .getLicenseManager(initLicenseParams()); // 安装证书 try { licenseManager.install(new File(licPath)); } catch (Exception e) { e.printStackTrace(); return false; } // 验证证书 try { //使用LicenseContent来接收默认校验的返回值,返回值为签名内容,进行二次校验 LicenseContent licenseContent = licenseManager.verify(); //扩展验证,非扩展验证可从licenseContent中获取 Map<String,String> content = (Map<String, String>) licenseContent.getExtra(); String targetMac = content.get("mac"); //此处用于存储最大用户数 String maxUserCount = content.get("maxUserCount"); result = validateMacAddress(targetMac); } catch (Exception e) { e.printStackTrace(); result = false; } return result; } 来自为知笔记(Wiz) 本文转载自:http://blog.csdn.net/luckymelina/article/details/22870665
标签路径 属性 说明 project name ant项目名称 project default 默认执行的ant任务 project basedir 指定默认的路径,'.'表示当前目录 project-path id 设置路径变量名 project-path-pathelement location 指定具体的jar文件或目录 project-path-fileset dir 指定目录 project-path-fileset includes 指定导入的文件,使用通配符*+后缀名 project-path-path refid 指定ant脚本中定义的路径 project-tstamp ant提供的时间戳的支持,默认包含DSTAMP/TSTAMP/TODAY三个变量默认的输出为格式为[DSTAMP -> 20170326,TSTAMP -> 1440,TODAY -> March 26 2017],详情 project-tstamp-format property 对应时间戳的名称 project-tstamp-format pattern 格式化方式 project-tstamp-format locale 根据地区输出时间格式,cn/zh/eg project-tstamp-format offset 设置时间延迟,正数为未来时间,负数为过去时间 project-tstamp-format unit 设置时间延迟的单位【millisecond、second、minute、hour、day、week、month、year】 project-property name 声明ant中的变量名 project-property value 声明ant中的变量值 project-target name 任务名称 project-target description 任务描述 project-target depends 依赖的ant任务,用于指定任务执行顺序 project-target-delete dir 删除目标目录 project-target-echo message 输出日志 project-target-mkdir dir 创建目录 project-target-javac 编译脚本 project-target-javac srcdir 指定编译目录 project-target-javac destdir 编译输出目录 project-target-javac debug 开关debug模式,用于支持spring指定注解 project-target-javac encoding 编译文件对应的编码 project-target-javac compiler 指定编译器,一般指定eclipse编译器,详情 project-target-javac source 原jdk版本 project-target-javac target 使用jdk编译版本 project-target-javac-classpath refid 指定申明的路径变量 project-target-java classname 执行java代码,用于执行特殊任务,指定java类的名称即可,运行的是java类中的main方法 project-target-java fork 知否另起线程执行该任务,另起的任务不会影响主任务执行结果 project-target-java-classpath refid 指定java任务对应的class路径 project-target-java-arg value 传入java方法的参数 <?xml version="1.0" encoding="utf-8"?> <project name="ex20170322" default="deleteWar" basedir="."> <path id="JAVA.rt"> <pathelement location="${JAVA_HOME}/common/rt.jar" /> </path> <path id="Server.libraryclasspath"> <fileset dir="D:\apache-tomcat-7.0.67-3\lib" includes="*.jar" /> </path> <path id="Project.libraryclasspath"> <fileset dir="${basedir}/WebRoot/WEB-INF/lib" includes="*.jar" /> </path> <path id="Project.classpath"> <path refid="JAVA.rt" /> <path refid="Server.libraryclasspath" /> <path refid="Project.libraryclasspath" /> </path> <tstamp> <format property="now" pattern="yyyyMMddhhmm" locale="zh"/> </tstamp> <property name="build" value="${basedir}/build" /> <property name="build.class" value="${build}/classes" /> <property name="src" value="${basedir}/src" /> <property name="webApp" value="${basedir}/WebRoot" /> <property name="lib" value="${webApp}/WEB-INF/lib" /> <property name="webserver" value="D:\Program Files\Apache Software Foundation\apache-tomcat-7.0.67\webapps" /> <target name="clean" description="delete dir"> <!-- <echo message="delete dir" /> --> <delete dir="${build}" /> </target> <target name="init" description="init project" depends="clean"> <echo message="init project" /> <mkdir dir="${build.class}" /> </target> <target name="compile" description="compile project" depends="init"> <echo message="compile project" /> <javac srcdir="${src}" destdir="${build.class}" debug="true" encoding="UTF-8" compiler="org.eclipse.jdt.core.JDTCompilerAdapter" source="1.6" target="1.6"> <classpath refid="Project.classpath" /> </javac> </target> <target name="generdir" depends="compile"> <echo message="build.class:${build.class}" /> <echo message="bugids:${bugids}" /> <echo message="beginTime:${beginTime}" /> <echo message="endTime:${endTime}" /> <java classname="com.fh.ant.GenerateBySvn" fork="yes" classpath="${build.class}"> <classpath refid="Project.libraryclasspath"/> <arg value="https://192.168.1.227:8443/svn/EX/branches/20170322"/> <arg value="mjm"/> <arg value="sbsb1234"/> <arg value="${build}"/> <arg value="${bugids}"/> <arg value="${now}"/> <arg value="${beginTime}"/> <arg value="${endTime}"/> </java> </target> <!--<target name="fabu" depends="generdir" description="fabu war">--> <!--<copy file="${build}/${ant.project.name}.war" todir="${webserver}"></copy>--> <!--</target>--> <target name="deleteWar" depends="generdir" description="delete compile result"> <delete dir="${build}" /> </target> </project> 来自为知笔记(Wiz)
2022年09月
2022年08月
2022年07月
2022年06月
2022年05月
2022年03月
2022年01月
2021年10月
2021年09月
2021年08月
学习了
@Query中是hql 一般这样写:select ua from UserAttendance ua where cardId=:cardid
如果要用原生sql要在注解中加上参数nativeQuery = true
貌似是现在不支持* 以前支持。
主要还是看自己把。如果掌握了新前端的一些框架可以用vue,react对应的ui控件集
如果不是 就找一个别人写好的架子把。 比如admintle ,layui 之类的。
数据量大了之后会变慢,特别是做联表查询时