前面我们讲到了Quartz框架在项目中的实现,在Quartz中的重要API有两个重要的触发器类:CronTrigger 和SimpleTrigger
在Quartz框架中这两个触发器都继承了一个抽象基类Trigger,这个类有触发器共有的属性name,jobName,group,jobGroup以及description;但是在spring框架中使用的触发器类是org.springframework.scheduling.quartz.CronTriggerBean ,该类的源码如下:
package org.springframework.scheduling.quartz;import java.text.ParseException;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.Constants;public class CronTriggerBean extends CronTriggerimplements JobDetailAwareTrigger, BeanNameAware, InitializingBean
{private static final Constants constants = new Constants(CronTrigger.class);private JobDetail jobDetail;private String beanName;public void setJobDataAsMap(Map jobDataAsMap){getJobDataMap().putAll(jobDataAsMap);}public void setMisfireInstructionName(String constantName){setMisfireInstruction(constants.asNumber(constantName).intValue());}public void setTriggerListenerNames(String[] names){for (int i = 0; i < names.length; i++)addTriggerListener(names[i]);}public void setJobDetail(JobDetail jobDetail){this.jobDetail = jobDetail;}public JobDetail getJobDetail() {return this.jobDetail;}public void setBeanName(String beanName) {this.beanName = beanName;}public void afterPropertiesSet() throws ParseException{if (getName() == null) {setName(this.beanName);}if (getGroup() == null) {setGroup("DEFAULT");}if (getStartTime() == null) {setStartTime(new Date());}if (getTimeZone() == null) {setTimeZone(TimeZone.getDefault());}if (this.jobDetail != null) {setJobName(this.jobDetail.getName());setJobGroup(this.jobDetail.getGroup());}}
}由此类源码可知,该触发器的核心代码还是在CronTrigger 类中。
但是在这个类中,可以在框架配置文件中设置属性jobDetail,至于另外一个属性cronExpression的设置方法是在类CronTrigger 中的。
public class CronTrigger extends Trigger
{......private CronExpression cronEx = null;......//由此可知在spring配置文件中,属性名是和set函数后面的字符串一样的,但首字母小写public void setCronExpression(String cronExpression) throws ParseException {TimeZone origTz = getTimeZone();this.cronEx = new CronExpression(cronExpression);this.cronEx.setTimeZone(origTz);......
}我们都知道spring框架IOC的容器管理为核心模块,配置文件的一些bean的设置都是为bean对象设置一些属性值来初始化,在此的触发器类分析,我们只关注触发器如何进行实践周期设置,时间字符串是如何解析的。
我们在spring配置文件中是只配置触发时间字符串的,而在CronTrigger类的public void setCronExpression(String cronExpression)内会根据这字符串来 CronExpression
对象,其中触发时间的解析就发生在这个类里面;
CronExpression类中解析后的结果存放在这些属性中:protected transient TreeSet seconds;//秒 protected transient TreeSet minutes;//分protected transient TreeSet hours;//时protected transient TreeSet daysOfMonth;//月中第几天protected transient TreeSet months;//月protected transient TreeSet daysOfWeek;//星期中第几天protected transient TreeSet years;//年protected transient boolean lastdayOfWeek = false;//是否为星期最后一天protected transient int nthdayOfWeek = 0;//protected transient boolean lastdayOfMonth = false;//月的最后一天,默认为false  protected transient boolean nearestWeekday = false;protected transient boolean expressionParsed = false;
这些存储时间信息的属性会在类CronExpression中的下面函数进行初始化protected void buildExpression(String expression)throws ParseException{//刚开始触发时间是否解析设置为false,开始解析则设置为truethis.expressionParsed = true;//初始化存储解析结果的属性try{if (this.seconds == null) {this.seconds = new TreeSet();}if (this.minutes == null) {this.minutes = new TreeSet();}if (this.hours == null) {this.hours = new TreeSet();}if (this.daysOfMonth == null) {this.daysOfMonth = new TreeSet();}if (this.months == null) {this.months = new TreeSet();}if (this.daysOfWeek == null) {this.daysOfWeek = new TreeSet();}if (this.years == null) {this.years = new TreeSet();}int exprOn = 0;//将触发时间字符串分割成6个字符串StringTokenizer exprsTok = new StringTokenizer(expression, " \t", false);//遍历这6个字符串,并分别处理,exprOn 从0-5依次增加,作为辨别字符串不同意义的识别码while ((exprsTok.hasMoreTokens()) && (exprOn <= 6)) {String expr = exprsTok.nextToken().trim();//对每个字符串进行格式判断,如果不正确就中断报错。if ((exprOn == 3) && (expr.indexOf('L') != -1) && (expr.length() > 1) && (expr.indexOf(",") >= 0)) {throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1);}if ((exprOn == 5) && (expr.indexOf('L') != -1) && (expr.length() > 1) && (expr.indexOf(",") >= 0)) {throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1);}//对每个字符串进行按“,”分割,并对每个分割后的字符串进行按识别码exprOn解析。StringTokenizer vTok = new StringTokenizer(expr, ",");while (vTok.hasMoreTokens()) {String v = vTok.nextToken();storeExpressionVals(0, v, exprOn);}exprOn++;}//如果出发时间字符串分割后的字符串数组大小少于5则中断报错,不符合语法if (exprOn <= 5) {throw new ParseException("Unexpected end of expression.", expression.length());}//因为出发时间格式可以为6个字符串,所以当处于这种情况的时候,就将第7个时间数据年设置成“*”,即每年的意义if (exprOn <= 6) {storeExpressionVals(0, "*", 6);}//get函数的方法体下面已经给出,指的是获取该类对象中存储触发器时间的存储结果。TreeSet dow = getSet(5);TreeSet dom = getSet(3);  boolean dayOfMSpec = !dom.contains(NO_SPEC);boolean dayOfWSpec = !dow.contains(NO_SPEC);if ((!dayOfMSpec) || (dayOfWSpec)){if ((!dayOfWSpec) || (dayOfMSpec)){throw new ParseException("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0);}}} catch (ParseException pe) {throw pe;} catch (Exception e) {throw new ParseException("Illegal cron expression format (" + e.toString() + ")", 0);}}由这个方法确定,最后的每个字符的意义解析由方法storeExpressionVals完成。
get方法:
/*
*   触发器时间解析后会存放在TreeSet属性中,这个函数就是获取这些属性的引用。
*/
protected TreeSet getSet(int type){switch (type) {case 0:return this.seconds;case 1:return this.minutes;case 2:return this.hours;case 3:return this.daysOfMonth;case 4:return this.months;case 5:return this.daysOfWeek;case 6:return this.years;}return null;}protected int storeExpressionVals(int pos, String s, int type)throws ParseException{int incr = 0;//由字符串开始位置遍历,跳过空字符,空字符包括空格,tab键,回车键等。int i = skipWhiteSpace(pos, s);//如果排除掉所有空字符后,只剩下一个字符或是没有字符,则方法返回,解析结束。if (i >= s.length()) {return i;}//获取第一个不为空的字符char c = s.charAt(i);//判断字符是否为字母,触发时间语法中,只有type为4,5的两项为字母,至于L(当月或这周最后一天) LW(当月最后一个工作日)情况在后面考虑if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW"))) {//前面提到,只有type为4,5的两项为字母,且这两项设置的值为字符个数为3的单词,即月份和周几单词的3个字母的缩写String sub = s.substring(i, i + 3);int sval = -1;int eval = -1;//当这项为月份时,通过getMonthNumber函数获取月份数据,在此类属性中,有关于月份单词与数字的映射,星期的映射也是一样。if (type == 4) {sval = getMonthNumber(sub) + 1;//月份数字不大于0报错,接下来的类似情况就不赘述了if (sval <= 0) {throw new ParseException("Invalid Month value: '" + sub + "'", i);}//如果该项字符数量不只为3,则该项形式就不是JAN(月份项)或者是SUN(星期项),就是JAN-DEC或者是SUN-SAT,下面就是解析第二种情况if (s.length() > i + 3) {c = s.charAt(i + 3);if (c == '-') {i += 4;//同样是截取3个字符月份单词,然后通过getMonthNumber函数获取月份数字数据sub = s.substring(i, i + 3);eval = getMonthNumber(sub) + 1;if (eval <= 0)throw new ParseException("Invalid Month value: '" + sub + "'", i);}}}//这个是星期项的数据解析,该项的解析和月份项的解析类似,读者可以自己分析else if (type == 5) {sval = getDayOfWeekNumber(sub);if (sval < 0) {throw new ParseException("Invalid Day-of-Week value: '" + sub + "'", i);}if (s.length() > i + 3) {c = s.charAt(i + 3);if (c == '-') {i += 4;sub = s.substring(i, i + 3);eval = getDayOfWeekNumber(sub);if (eval < 0) {throw new ParseException("Invalid Day-of-Week value: '" + sub + "'", i);}}//对于星期项,这个字符为#的时候,后面跟着的一个数字表示这个月的第几个星期。else if (c == '#') {try {i += 4;this.nthdayOfWeek = Integer.parseInt(s.substring(i));//因为一个月最多横跨5个星期,不可能横跨6个以上的星期。if ((this.nthdayOfWeek < 1) || (this.nthdayOfWeek > 5))throw new Exception();}catch (Exception e) {throw new ParseException("A numeric value between 1 and 5 must follow the '#' option", i);}}//如果该项这个字符为L时,表示是该月的最后一个星期,前面加数字或单词表示最后一个星期的星期几else if (c == 'L') {this.lastdayOfWeek = true;i++;}}}else {throw new ParseException("Illegal characters for this position: '" + sub + "'", i);}if (eval != -1) {//这个是将当每一项未设置递增项时候,默认为1,如月份项为0-8,那么此时会按1从0到8递增将数字0,1,2,3,4....8放入月份TreeSet数据集合中。incr = 1;}addToSet(sval, eval, incr, type);return i + 3;}//如果首个字符为?if (c == '?') {i++;//语法规定,?后面不会再有其他字符或数字,所以此处解析中断跳出处理异常。if ((i + 1 < s.length()) && (s.charAt(i) != ' ') && (s.charAt(i + 1) != '\t')){throw new ParseException("Illegal character after '?': " + s.charAt(i), i);}//?字符只能用于dayofmonth和dayofweek这两项,即type为3和5的两项if ((type != 5) && (type != 3)) {throw new ParseException("'?' can only be specfied for Day-of-Month or Day-of-Week.", i);}//因为在这两项中,如果dayofmonth项已经有了?,那么在dayofweek就不应该有?了。if ((type == 5) && (!this.lastdayOfMonth)) {int val = ((Integer)this.daysOfMonth.last()).intValue();if (val == 98) {throw new ParseException("'?' can only be specfied for Day-of-Month -OR- Day-of-Week.", i);}}//98表示字符为?,99表示字符为*;姑且是猜测。addToSet(98, -1, 0, type);return i;}//如果该项第一个字符为*或者是/的时候if ((c == '*') || (c == '/')) {//此情况是该项只有*,后面就没有了其他字符或数字if ((c == '*') && (i + 1 >= s.length())) {//由此能证实99就是表示*字符的。addToSet(99, -1, incr, type);return i + 1;}//这种情况是字符为“/”的情况下,如果后面没有了字符或者后面的字符为空,解析就中断报错,抛出异常。if ((c == '/') && ((i + 1 >= s.length()) || (s.charAt(i + 1) == ' ') || (s.charAt(i + 1) == '\t'))){throw new ParseException("'/' must be followed by an integer.", i);}//字符为*,且后面还有不为空的字符if (c == '*') {i++;}c = s.charAt(i);//*字符后面的字符为/if (c == '/') {i++;//如果“/”字符后面没有了字符,就抛出异常。if (i >= s.length()) {throw new ParseException("Unexpected end of string.", i);}//得到“/”后面的数字,这个数字为递增量incr = getNumericValue(s, i);i++;if (incr > 10) {i++;}//下面是在不同type下对递增量incr进行判断,每一项都会有最大的值,因此incr的值不能超过这些值if ((incr > 59) && ((type == 0) || (type == 1)))throw new ParseException("Increment > 60 : " + incr, i);if ((incr > 23) && (type == 2))throw new ParseException("Increment > 24 : " + incr, i);if ((incr > 31) && (type == 3))throw new ParseException("Increment > 31 : " + incr, i);if ((incr > 7) && (type == 5))throw new ParseException("Increment > 7 : " + incr, i);if ((incr > 12) && (type == 4))throw new ParseException("Increment > 12 : " + incr, i);}//首字符为*字符的后面没有“/”的话,递增量incr设置为1.else {incr = 1;}//99表示的是*addToSet(99, -1, incr, type);return i;}//如果首字符为L,表示本月最后一天或是一个星期的最后一天if (c == 'L') {i++;if (type == 3) {this.lastdayOfMonth = true;}if (type == 5) {addToSet(7, 7, 0, type);}//在dayofmonth项,L和W可以合着一起用,表示获取离设置时间最近的工作日if ((type == 3) && (s.length() > i)) {c = s.charAt(i);if (c == 'W') {this.nearestWeekday = true;i++;}}return i;}//如果该项首字符为数字,则该项形式有0,0-4, 10, 10-14,0/2等等if ((c >= '0') && (c <= '9')) {int val = Integer.parseInt(String.valueOf(c));i++;//如果只是0这种形式,就直接将这个放到TreeSet数据集合中去if (i >= s.length()) {addToSet(val, -1, -1, type);} else {//如果是其他形式,就获取多位数字,并调用checkNext函数,即checkNext函数是解析数字形式的函数。c = s.charAt(i);if ((c >= '0') && (c <= '9')) {ValueSet vs = getValue(val, s, i);val = vs.value;i = vs.pos;}i = checkNext(i, s, val, type);return i;}} else {throw new ParseException("Unexpected character: " + c, i);}return i;}
对于这个函数都是如何解析触发时间字符串的,我将在函数中的每段代码这个做详细的解释。下面给出的是触发时间的语法:
Cron表达式的格式:秒 分 时 日 月 周 年(可选)。
字段名              允许的值                    允许的特殊字符
秒                     0-59                           , - * /
分                     0-59                           , - * /
小时                  0-23                           , - * /
日                     1-31                            , - * ? / L W C
月                     1-12 or JAN-DEC        , - * /
周几                  1-7 or SUN-SAT         , - * ? / L C #
年(可选字段)     empty                         1970-2099 , - * /
为了获取更加详细的语法解释,请读者朋友可以在这个链接的文章学习http://blog.csdn.net/eacter/article/details/44308459,这篇文章解释的非常详细;
checkNext函数的源代码:protected int checkNext(int pos, String s, int val, int type)throws ParseException{int end = -1;int i = pos;//在之前已经处理了数为一位的情况,这个if内代码处理的是数为两位的情况if (i >= s.length()) {addToSet(val, end, -1, type);return i;}char c = s.charAt(pos);//继续解析数字后面的字符,如果为L的话if (c == 'L') {//如果是在dayofweek项,则设置最后一个星期属性为trueif (type == 5) {if ((val < 1) || (val > 7))throw new ParseException("Day-of-Week values must be between 1 and 7", -1);this.lastdayOfWeek = true;} else {throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i);}//将数放入dayofweek项的TreeSet数据集合中去TreeSet set = getSet(type);set.add(new Integer(val));i++;return i;}//继续解析数字后面的字符,如果为w的话,w只能用在dayofmonth项,则设置靠近设置时间的工作日(nearestWeekday = true)为trueif (c == 'W') { if (type == 3) this.nearestWeekday = true; else { throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); } TreeSet set = getSet(type); set.add(new Integer(val)); i++; return i; }//继续解析数字后面的字符,如果为#的话,#只能用于dayofweek项,后面的数表示一个月的第几个星期if (c == '#') { if (type != 5) { throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); } i++; try {//设置第几个星期值this.nthdayOfWeek = Integer.parseInt(s.substring(i));if ((this.nthdayOfWeek < 1) || (this.nthdayOfWeek > 5))throw new Exception();}catch (Exception e) {throw new ParseException("A numeric value between 1 and 5 must follow the '#' option", i);}TreeSet set = getSet(type);set.add(new Integer(val));i++;return i;}//-用于两个数之间,表示两个数的范围内,如果第二个数后面没有/的话,递增量默认为1if (c == '-') {i++;c = s.charAt(i);int v = Integer.parseInt(String.valueOf(c));end = v;i++;if (i >= s.length()) {addToSet(val, end, 1, type);return i;}c = s.charAt(i);if ((c >= '0') && (c <= '9')) {//获取-右边的数ValueSet vs = getValue(v, s, i);int v1 = vs.value;end = v1;i = vs.pos;}//第二个数后面跟了/的情况if ((i < s.length()) && ((c = s.charAt(i)) == '/')) {i++;c = s.charAt(i);//获取递增量,递增量为一位的数情况int v2 = Integer.parseInt(String.valueOf(c));i++;if (i >= s.length()) {addToSet(val, end, v2, type);return i;}c = s.charAt(i);if ((c >= '0') && (c <= '9')) {//获取递增量,递增量为两位的数情况ValueSet vs = getValue(v2, s, i); int v3 = vs.value; addToSet(val, end, v3, type); i = vs.pos; return i; }
//当数为1位数时,但是后面字符为空的情况addToSet(val, end, v2, type); return i; }//将默认递增量为1插入TreeSet中 addToSet(val, end, 1, type); return i; }//数字之后字符为/,并获取递增量 if (c == '/') { i++; c = s.charAt(i); int v2 = Integer.parseInt(String.valueOf(c)); i++;if (i >= s.length()) {addToSet(val, end, v2, type);return i;}c = s.charAt(i);if ((c >= '0') && (c <= '9')) {ValueSet vs = getValue(v2, s, i);int v3 = vs.value;addToSet(val, end, v3, type);i = vs.pos;return i;}throw new ParseException("Unexpected character '" + c + "' after '/'", i);}//当数为1位数时,但是后面字符为空的情况addToSet(val, end, 0, type);i++;return i;}
这些都语法解析的关键代码,至于addToSet函数,它的功能是将解析后的结果放到各项TreeSet数据集合中去。addToSet函数源码如下
protected void addToSet(int val, int end, int incr, int type)throws ParseException{//根据type值获取TreeSet数据集合TreeSet set = getSet(type);//根据type值的不同分别判断val的值是否在各项的值范围内//当时间项是秒和分项的时候,最小值为0 ,最大值为59if ((type == 0) || (type == 1)) {if (((val < 0) || (val > 59) || (end > 59)) && (val != 99)) {throw new ParseException("Minute and Second values must be between 0 and 59", -1);}}//如果时间项为时,最小值为0,最大值为23else if (type == 2) {if (((val < 0) || (val > 23) || (end > 23)) && (val != 99)) {throw new ParseException("Hour values must be between 0 and 23", -1);}}//时间项为dayofmonth,最小值为1,最大为31else if (type == 3) {if (((val < 1) || (val > 31) || (end > 31)) && (val != 99) && (val != 98)){throw new ParseException("Day of month values must be between 1 and 31", -1);}}//时间项为月份时,最小值为1,最大值为12else if (type == 4) {if (((val < 1) || (val > 12) || (end > 12)) && (val != 99)) {throw new ParseException("Month values must be between 1 and 12", -1);}}//dayofweek项,范围是1-7else if ((type == 5) && ((val == 0) || (val > 7) || (end > 7)) && (val != 99) && (val != 98)){throw new ParseException("Day-of-Week values must be between 1 and 7", -1);}if (((incr == 0) || (incr == -1)) && (val != 99)) {if (val != -1)set.add(new Integer(val));else {set.add(NO_SPEC);}return;}int startAt = val;int stopAt = end;//如果所有项没有设置最终值,则将最终值都默认设置为最大值,最小值如果没有设置,则将都默认设置为最小值//且在“*”的情况下,如果增量没有设置的话,递增量默认设置为1if ((val == 99) && (incr <= 0)) {incr = 1;set.add(ALL_SPEC);}if ((type == 0) || (type == 1)) {if (stopAt == -1) {stopAt = 59;}if ((startAt == -1) || (startAt == 99))startAt = 0;}else if (type == 2) {if (stopAt == -1) {stopAt = 23;}if ((startAt == -1) || (startAt == 99))startAt = 0;}else if (type == 3) {if (stopAt == -1) {stopAt = 31;}if ((startAt == -1) || (startAt == 99))startAt = 1;}else if (type == 4) {if (stopAt == -1) {stopAt = 12;}if ((startAt == -1) || (startAt == 99))startAt = 1;}else if (type == 5) {if (stopAt == -1) {stopAt = 7;}if ((startAt == -1) || (startAt == 99))startAt = 1;}else if (type == 6) {if (stopAt == -1) {stopAt = 2299;}if ((startAt == -1) || (startAt == 99)) {startAt = 1970;}}int max = -1;if (stopAt < startAt) {switch (type) { case 0:max = 60; break;case 1:max = 60; break;case 2:max = 24; break;case 4:max = 12; break;case 5:max = 7; break;case 3:max = 31; break;case 6:throw new IllegalArgumentException("Start year must be less than stop year");default:throw new IllegalArgumentException("Unexpected type encountered");}stopAt += max;}for (int i = startAt; i <= stopAt; i += incr)if (max == -1){set.add(new Integer(i));}else {int i2 = i % max;//此处代码type只有4,5,3满足条件,因为max的值,在这三种情况下都能取的得到,其他type下是无法取到的。if ((i2 == 0) && ((type == 4) || (type == 5) || (type == 3))) {i2 = max;}set.add(new Integer(i2));}}这个函数根据传进来的范围值和递增量以及type值,将各个时间项 的所有值都放到TreeSet集合找那个去,当任务调度器实时地将时间与这些TreeSet集合内的时间信息做对比,
如果时间对上后,就会触发跑批任务。总结:CronTrigger类是时间点跑批任务设置的类,本问终点分析的是哪一个类对时间进行解析,即CronTrigger类继承的类Trigger中的属性CronExpression类。时间的语法
解析都是在这个类中完成,得到的结果是7个TreeSet数据集合属性,分别存放的是seconds ,minutes,hours,dayOfMonth,months,dayOfWeek,years等属性中,这些集合存放的数据
都是整数数据,即跑批任务触发时间点的各项(年月日时分秒)数据,scheduler对象在每秒都会比对当前时间和存放的触发时间信息,如果吻合就触发跑批任务对象。

talk is cheap  ,show you the code and the doc,更多的分享内容请关注我的工作号:大白共图社。公众号会有很多的github开源社区拿来即用项目源码以及相关的文章。欢迎关注。

spring Quartz 源码分析--触发器类CronTriggerBean源码剖析相关推荐

  1. Flume 1.7 源码分析(一)源码编译

    Flume 1.7 源码分析(一)源码编译 Flume 1.7 源码分析(二)整体架构 Flume 1.7 源码分析(三)程序入口 1 说明 Flume是Cloudera提供的一个高可用的,高可靠的, ...

  2. Storm源码分析之四: Trident源码分析

    Storm源码分析之四: Trident源码分析 @(STORM)[storm] Storm源码分析之四 Trident源码分析 一概述 0小结 1简介 2关键类 1Spout的创建 2spout的消 ...

  3. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 二 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  4. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  5. 【OkHttp】OkHttp 源码分析 ( OkHttpClient.Builder 构造器源码分析 )

    OkHttp 系列文章目录 [OkHttp]OkHttp 简介 ( OkHttp 框架特性 | Http 版本简介 ) [OkHttp]Android 项目导入 OkHttp ( 配置依赖 | 配置 ...

  6. 【SemiDrive源码分析】【Yocto源码分析】02 - yocto/meta-openembedded目录源码分析

    [SemiDrive源码分析][Yocto源码分析]02 - yocto/meta-openembedded目录源码分析 一.meta-openembedded 目录 本 SemiDrive源码分析 ...

  7. spring调用切面失效分析(类内调用自身切面方法或其他切面方法失效)

    spring调用切面失效分析(类内调用自身切面方法或其他切面方法失效) 简述 示例代码说明 演奏钢琴 观众鼓掌 执行程序及结果 原因分析 Spring切面原理 代码调试 fix bug 代码 结果 简 ...

  8. 【SemiDrive源码分析】【Yocto源码分析】07 - core-image-base-x9h_ref_serdes.rootfs.ext4 文件系统是如何生成的

    [SemiDrive源码分析][Yocto源码分析]07 - core-image-base-x9h_ref_serdes.rootfs.ext4 文件系统是如何生成的 1.core-image-ba ...

  9. Java源码详解六:ConcurrentHashMap源码分析--openjdk java 11源码

    文章目录 注释 类的继承与实现 数据的存储 构造函数 哈希 put get 扩容 本系列是Java详解,专栏地址:Java源码分析 ConcurrentHashMap 官方文档:ConcurrentH ...

最新文章

  1. php 常用设计模式demo
  2. 14行代码AC_Zero Array(思维)
  3. ASP.NET Core 实现基于 ApiKey 的认证
  4. jzoj3462-休息【归并排序,逆序对】
  5. 从零开始学前端:css3新属性scss和less --- 今天你学习了吗?(CSS:Day22)
  6. 解决Admob Banner首次展示不显示的问题
  7. H5中CSS3动画的性能优化
  8. three.js OrbitControls鼠标按键修改(左平移,右旋转)
  9. 2020-11-17 一道有趣的求极限问题
  10. SpringBoot+Vue的房屋租赁系统(含前后台)
  11. windows通过bat批处理命令,快速清理系统垃圾文件,释放硬盘空间
  12. 电路分析 极简复习指导、公式推导、常用结论归纳 第十章 含有耦合电感的电路
  13. 智能挪车v4.8.2-多开版
  14. flv是什么视频格式?怎么把flv转换成mp4?
  15. linux 手机互传,轻松实现Linux笔记本和手机、PDA互传文件(转)
  16. 前端判断文件后缀名_js判断上传文件后缀名是否合法
  17. [软考知识点总结③] 【中级软件设计师】计算机组成原理——校验码、海明校验码、奇偶校验码、循环校验码
  18. 计算机教室100字介绍,描写教室的作文100字
  19. 一篇文章读懂身份技术发展简史
  20. SpringBoot-yaml语法规则和读取数据

热门文章

  1. Webpack 学这篇就够了,入门到上线优化
  2. 记淘宝客、多多客api开发系列。一、淘宝联盟淘宝客api对接
  3. 泰坦尼克号幸存者预测(案例)----决策树版
  4. 计算机开机桌面一直转圈,电脑开机卡在品牌logo转圈界面,请问如何解决。
  5. Python面试题:都在这里了【315+道题】
  6. 知乎live笔记09 程序员跳槽时,如何优雅地谈薪水?
  7. pagehelper联表分页查询
  8. JAVA 堆栈的区别
  9. 中国移动蔡谦:5G传输准备就绪
  10. 【IoT】创业:如何找到可以主导的创业市场?