Fork me on GitHub

天文

天文历法

阴历 与 阳历

阳历,也叫太阳历,制定的基础是地球绕太阳公转的运动周期:地球绕太阳一圈,就是一年。现在我们用的“公历”,就是太阳历的一种。

阴历被称作“月亮历”,是依据月亮的变化来修订的历法。月亮每经历一次从圆到缺的循环,就是一个月。

而农历实际上是一种兼顾太阳历和月亮历的历法,叫作阴阳历。这才是中国古人一直沿用的历法,也是我们现在经常说的“阴历”(其实叫错了……)

补充:农历为什么不是阴历

在中国古代,一开始是使用阴历的,毕竟看月亮比看太阳还是方便不少。但是祖先们的主要岗位职能是种地,而月亮就不能提供农作物生长所需要的热量,这时看太阳就显得尤其的重要。另外,阴历的一年大约354天,与太阳周期(阳历)的一年差了11天,这样就会慢慢产生一些差异。一年两年可能还没什么感觉,可是过着过着,大家就从冬天过年变成了夏天过年,而且原本应该春天播种,也会因为这个差异顺延到秋天,这日子就没法过了。

于是勤劳智慧的祖先们又把阴历与阳历结合在一起,加上了闰月,创造出了农历。

农历是根据太阳和月亮的运行确定的,根据月亮确定每个月,根据太阳确定一年,所以农历不是阴历或阳历,农历是阴阳历。节气根据太阳确定是为了指导农业生产,这和农不农历没有冲突。

所谓的阳历就是公历,与二十四节气无关,真正按二十四节气计日的历法只有干支历,是黄帝创造并开始使用的,十二个节气日分别为十二个月的头一天,中气日在月中,如立春日为正月第一天,惊蛰日为二月第一天,清明日为三月第一天,…,八字,算命,择吉日都在使用这种干支历,也叫“老黄历”。

四季

农历 正月/二月/三月为春季,分别称为 孟春/仲春/季春。

农历 四月/五月/六月为夏季,分别称为 孟夏/仲夏/季夏。

农历 七月/八月/九月为秋季,同理。

农历 十月/十一月/十二月为冬季,同理。

二十四节气

24节气的确立既不是根据我们现在所说的阳历,也不是阴历来制定的,而是根据当初的干支历

古人把地球绕太阳运行的轨道命名为“黄道”,也称“日道”,将黄道等比例分成24份,之间相隔15° ,就形成了24个节气。二十四个节气,代表着地球在“黄道”上的二十四个不同位置。而每个节气,都表示着气候、物候、时候,这“三候”的不同变化。

春:立春/雨水——二月;惊蛰/春分——三月;清明/谷雨——四月;

夏:立夏/小满——五月;芒种/夏至——六月;小暑/大暑——七月;

秋:立秋/处暑——八月;白露/秋分——九月;寒露/霜降——十月;

冬:立冬/小雪—十一月;大雪/冬至—十二月;小寒/大寒——一月;

春雨惊春清谷天

夏满芒夏暑相连

秋处露秋寒霜降

冬雪雪冬小大寒

表示一年四季中每个季节的开始,立春/立夏/立秋/立冬合为四立

表示极/最的意思。夏至/冬至表示夏天/冬天的到来。夏至北半球白昼最长;冬至北半球白昼最短;

表示平分的意思。春分/秋分表示昼夜等长。平分昼夜。

天干地支纪年法

天干地支十干十二支的合称、简通称为十天干十二地支,由两者经一定的组合方式搭配成六十对,为一个周期,循环往复,称为六十甲子六十花甲子年

古代中国用以记录年、月、日、时。

阴阳五行方位与天干地支

西

干支纪年

原则要求天干与地支配对使用,两两相配,始于甲子,终于癸亥,六十为一循环。

根据干支的构成条件,其循环周期必然是天干数和地干数的最小公倍数。而60正是10和12的最小公倍数。

1甲子 2乙丑 3丙寅 4丁卯 5戊辰 6己巳 7庚午 8辛未 9壬申 10癸酉
11甲戌 12乙亥 13丙子 丁丑 戊寅 己卯 庚辰 辛巳 壬午 20癸未
21甲申 乙酉 丙戌 丁亥 戊子 己丑 庚寅 辛卯 壬辰 30癸巳
31甲午 乙未 丙申 丁酉 戊戌 己亥 庚子 辛丑 壬寅 40癸卯
41甲辰 乙巳 丙午 丁未 戊申 己酉 庚戌 辛亥 壬子 50癸丑
51甲寅 乙卯 丙辰 丁巳 戊午 己未 庚申 辛酉 壬戌 60癸亥

由序号得到对应干支是很容易的,序号除以10的余数就是天干的序数(如果余数是0,则为最后一个天干癸),序号除以12的余数就是地支的序数(如果余数是0,则为最后一个地支亥)。比如37号干支,因为37 mod 10=7(mod表示取余数),对应的天干是庚,37 mod 12=1,对应的地支是子,所以37号干支就是庚子。显然,一个整数除以10的余数就是它的个位数,这就使求天干更方便了。

而由干支推它的序号,也不困难。这其实就是一个同余方程组的求解问题,我们用初等数论中的中国剩余定理就可以解决。比如要算戊午的序号是多少,根据上面由序号得到对应干支的原理,很容易得到如下方程组:

{ x mod 10 = 5
{ x mod 12 = 7.

其中x是待求的干支序号。根据中国剩余定理,有:

x ≡ 6 * 5 - 5 * 7 (mod 60) = 55,

即戊午的序号是55.这和上面的对照表的是一致的。一般地,若天干的序号为m,地支的序号为n,则干支的序号为:

x ≡ 6m - 5n (mod 60) (1)

简单点说,如果6m-5n的结果是正数,这个数就是干支的序号;如果是负数,把它加上60就是干支的序号。

干支纪年转西历

天干用序号1至10表示甲到癸,地支用1至12对应子到亥。

天干用序号1至10表示甲到癸,地支用1至12对应子到亥。

6×天干 - 5×地支+3(或1983)+60n = 西历年份
即,设Year是西元后某个年份,H(heaven的首字母)是Year的天干(甲=1, 乙=2, 丙=3,….., 癸=10),E(earth的首字母)是Year的地支(子=1, 丑=2, 寅=3,….., 亥=12),则
{\displaystyle \mathrm {Year} \equiv 6H-5E+3{\pmod {60}}}

西历转干支纪年

公元后纪年换算公式:

将西元后年分减3,再除以10,所得余数即为第几个天干(余0则视为第10个);将西元后年分减3,再除以12,所得余数即为第几个地支(余0则视为第12个)。

0 1 2 3 4 5 6 7 8 9 10 11 12
天干
地支
  • 例如:公元1995年

1995-3=1992,1992除以10的余数是2,所以是第2个天干——乙,1992除以12的余数是0,所以是第12个地支——亥,故公元1995年为乙亥年。

  • 再如:公元1861年

1861-3=1858,1858除以10的余数是8,所以是第8个天干——辛,1858除以12的余数是10,所以是第10个地支——酉,故公元1861年为辛酉年。

年份尾数与天干关系
0 1 2 3 4 5 6 7 8 9
天干
地支

公元前纪年与干支纪年的换算表:

0 1 2 3 4 5 6 7 8 9 10 11
天干
地支

如:公元前155年

尾数5对应天干中的“丙”; 155除以12得余数11,对应地支中的“戌”。该年为丙戌年。 公元前8年则为癸丑年;公元前2年为己未年。

干支纪月

年上起月的歌诀,《五虎遁月歌》:甲己之年丙作初,乙庚之岁戊为头,丙辛岁首从庚起,丁壬壬位顺流行,若问戊癸何方法,甲寅之上好推求。

具体的推算如下表[9]

月地支 节气段 中气 近似夏历月份 近似西历月份 甲或己年 乙或庚年 丙或辛年 丁或壬年 戊或癸年
寅月 立春至惊蛰前一日 雨水 正月 2月 丙寅月 戊寅月 庚寅月 壬寅月 甲寅月
卯月 惊蛰至清明前一日 春分 二月 3月 丁卯月 己卯月 辛卯月 癸卯月 乙卯月
辰月 清明至立夏前一日 谷雨 三月 4月 戊辰月 庚辰月 壬辰月 甲辰月 丙辰月
巳月 立夏至芒种前一日 小满 四月 5月 己巳月 辛巳月 癸巳月 乙巳月 丁巳月
午月 芒种至小暑前一日 夏至 五月 6月 庚午月 壬午月 甲午月 丙午月 戊午月
未月 小暑至立秋前一日 大暑 六月 7月 辛未月 癸未月 乙未月 丁未月 己未月
申月 立秋至白露前一日 处暑 七月 8月 壬申月 甲申月 丙申月 戊申月 庚申月
酉月 白露至寒露前一日 秋分 八月 9月 癸酉月 乙酉月 丁酉月 己酉月 辛酉月
戌月 寒露至立冬前一日 霜降 九月 10月 甲戌月 丙戌月 戊戌月 庚戌月 壬戌月
亥月 立冬至大雪前一日 小雪 十月 11月 乙亥月 丁亥月 己亥月 辛亥月 癸亥月
子月 大雪至小寒前一日 冬至 十一月 12月 丙子月 戊子月 庚子月 壬子月 甲子月
丑月 小寒至立春前一日 大寒 十二月 1月 丁丑月 己丑月 辛丑月 癸丑月 乙丑月

干支纪日

太过复杂。

干支纪时

时辰地支 UTC+8 甲或己日 乙或庚日 丙或辛日 丁或壬日 戊或癸日
子时 23时至1时 甲子时 丙子时 戊子时 庚子时 壬子时
丑时 1时至3时 乙丑时 丁丑时 己丑时 辛丑时 癸丑时
寅时 3时至5时 丙寅时 戊寅时 庚寅时 壬寅时 甲寅时
卯时 5时至7时 丁卯时 己卯时 辛卯时 癸卯时 乙卯时
辰时 7时至9时 戊辰时 庚辰时 壬辰时 甲辰时 丙辰时
巳时 9时至11时 己巳时 辛巳时 癸巳时 乙巳时 丁巳时
午时 11时至13时 庚午时 壬午时 甲午时 丙午时 戊午时
未时 13时至15时 辛未时 癸未时 乙未时 丁未时 己未时
申时 15时至17时 壬申时 甲申时 丙申时 戊申时 庚申时
酉时 17时至19时 癸酉时 乙酉时 丁酉时 己酉时 辛酉时
戌时 19时至21时 甲戌时 丙戌时 戊戌时 庚戌时 壬戌时
亥时 21时至23时 乙亥时 丁亥时 己亥时 辛亥时 癸亥时

阴阳

阴 与 阳

五行

五行中每一行都有不同性能。“木曰曲直”,意思是木具有生长、升发之特性;“火曰炎上”,是火具有发热、向上之特性;“土曰稼穑”,是指土具有种植庄稼,生化万物之特性;“金曰从革”,是金具有肃杀、变革之特性;“水曰润下”,是水具有滋润、向下之特性。古人基于此种认识,将宇宙间各种事物分别归属于五行,因此在概念上,已经不是木、火、土、金、水本身,而是一大类在特性上可相比拟之各种事物、现象所共有之抽象性能。

六气

风/寒/暑/湿/燥/火

参考

二十四节气

年份推断干支纪年和纪日

spring-aop

spring-aop

概念

面向切面的程序设计 (Aspect-oriented programming,AOP,又译作面向方面的程序设计剖面导向程序设计 )是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。

  • Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
  • Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
  • Pointcut:切入点,即一组连接点的集合;
  • Advice:增强,指特定连接点上执行的动作;
  • Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
  • Weaving:织入,指将切面整合到程序的执行流程中;
  • Interceptor:拦截器,是一种实现增强的方式;
  • Target Object:目标对象,即真正执行业务的核心逻辑对象;
  • AOP Proxy:AOP代理,是客户端持有的增强后的对象引用。

AOP原理

在Java平台上,对于AOP的织入,有3种方式:

  1. 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入;
  2. 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
  3. 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。

最简单的方式是第三种,Spring的AOP实现就是基于JVM的动态代理。由于JVM的动态代理要求必须实现接口,如果一个普通类没有业务接口,就需要通过CGLIB或者Javassist这些第三方库实现。

我们以LoggingAspect.doAccessCheck()为例,要把它注入到UserService的每个public方法中,最简单的方法是编写一个子类,并持有原始实例的引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UserServiceAopProxy extends UserService {
private UserService target;
private LoggingAspect aspect;

public UserServiceAopProxy(UserService target, LoggingAspect aspect) {
this.target = target;
this.aspect = aspect;
}

public User login(String email, String password) {
// 先执行Aspect的代码:
aspect.doAccessCheck();
// 再执行UserService的逻辑:
return target.login(email, password);
}

public User register(String email, String password, String name) {
aspect.doAccessCheck();
return target.register(email, password, name);
}

...
}

这些都是Spring容器启动时为我们自动创建的注入了Aspect的子类,它取代了原始的UserService(原始的UserService实例作为内部变量隐藏在UserServiceAopProxy中)。如果我们打印从Spring容器获取的UserService实例类型,它类似UserService$$EnhancerBySpringCGLIB$$1f44e01c,实际上是Spring使用CGLIB动态创建的子类,但对于调用方来说,感觉不到任何区别.

Spring对接口类型使用JDK动态代理,对普通类使用CGLIB创建子类。如果一个Bean的class是final,Spring将无法为其创建子类。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Aspect
@Component
public class LoggingAspect {
// 在执行UserService的每个方法前执行:
@Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")
public void doAccessCheck() {
System.err.println("[Before] do access check...");
}

// 在执行MailService的每个方法前后执行:
@Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))")
public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
System.err.println("[Around] start " + pjp.getSignature());
Object retVal = pjp.proceed();
System.err.println("[Around] done " + pjp.getSignature());
return retVal;
}
}

观察doAccessCheck()方法,我们定义了一个@Before注解,后面的字符串是告诉AspectJ应该在何处执行该方法,这里写的意思是:执行UserService的每个public方法前执行doAccessCheck()代码。

再观察doLogging()方法,我们定义了一个@Around注解,它和@Before不同,@Around可以决定是否执行目标方法,因此,我们在doLogging()内部先打印日志,再调用方法,最后打印日志后返回结果。

LoggingAspect类的声明处,除了用@Component表示它本身也是一个Bean外,我们再加上@Aspect注解,表示它的@Before标注的方法需要注入到UserService的每个public方法执行前,@Around标注的方法需要注入到MailService的每个public方法执行前后。

紧接着,我们需要给@Configuration类加上一个@EnableAspectJAutoProxy注解:

1
2
3
4
5
6
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AppConfig {
...
}

Spring的IoC容器看到这个注解,就会自动查找带有@Aspect的Bean,然后根据每个方法的@Before@Around等注解把AOP注入到特定的Bean中。执行代码,我们可以看到以下输出:

1
2
3
4
5
6
7
8
[Before] do access check...
[Around] start void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User)
Welcome, test!
[Around] done void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User)
[Before] do access check...
[Around] start void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)
Hi, Bob! You are logged in at 2020-02-14T23:13:52.167996+08:00[Asia/Shanghai]
[Around] done void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)

拦截器类型

顾名思义,拦截器有以下类型:

  • @Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了;
  • @After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行;
  • @AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码;
  • @AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;
  • @Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。

使用注解拦截

使用注解实现AOP需要先定义注解,然后使用@Around("@annotation(name)")实现装配;

使用注解既简单,又能明确标识AOP装配,是使用AOP推荐的方式.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Target(METHOD)
@Retention(RUNTIME)
public @interface MetricTime {
String value();
}

@Component
public class UserService {
// 监控register()方法性能:
@MetricTime("register")
public User register(String email, String password, String name) {
...
}
...
}

@Aspect
@Component
public class MetricAspect {
@Around("@annotation(metricTime)")
public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
String name = metricTime.value();
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long t = System.currentTimeMillis() - start;
// 写入日志或发送至JMX:
System.err.println("[Metrics] " + name + ": " + t + "ms");
}
}
}

注意metric()方法标注了@Around("@annotation(metricTime)"),它的意思是,符合条件的目标方法是带有@MetricTime注解的方法,因为metric()方法参数类型是MetricTime(注意参数名是metricTime不是MetricTime),我们通过它获取性能监控的名称。

有了@MetricTime注解,再配合MetricAspect,任何Bean,只要方法标注了@MetricTime注解,就可以自动实现性能监控.

问题

aop的坑,根本上是动态代理的坑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@Component
public class UserService {
// 成员变量:
public final ZoneId zoneId = ZoneId.systemDefault();

// 构造方法:
public UserService() {
System.out.println("UserService(): init...");
System.out.println("UserService(): zoneId = " + this.zoneId);
}

// public方法:
public ZoneId getZoneId() {
return zoneId;
}

// public final方法:
public final ZoneId getFinalZoneId() {
return zoneId;
}
}

@Component
public class MailService {
@Autowired
UserService userService;

public String sendMail() {
ZoneId zoneId = userService.zoneId;
String dt = ZonedDateTime.now(zoneId).toString();
return "Hello, it is " + dt;
}
}

@Configuration
@ComponentScan
public class AppConfig {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MailService mailService = context.getBean(MailService.class);
System.out.println(mailService.sendMail());
}
}

// 输出正常
UserService(): init...
UserService(): zoneId = Asia/Shanghai
Hello, it is 2020-04-12T10:23:22.917721+08:00[Asia/Shanghai]
/***************************演示aop的坑************************************/
@Aspect
@Component
public class LoggingAspect {
@Before("execution(public * com..*.UserService.*(..))")
public void doAccessCheck() {
System.err.println("[Before] do access check...");
}
}

Exception in thread "main" java.lang.NullPointerException: zone
at java.base/java.util.Objects.requireNonNull(Objects.java:246)
at java.base/java.time.Clock.system(Clock.java:203)
at java.base/java.time.ZonedDateTime.now(ZonedDateTime.java:216)
at com.itranswarp.learnjava.service.MailService.sendMail(MailService.java:19)
at com.itranswarp.learnjava.AppConfig.main(AppConfig.java:21)

@Component
public class MailService {
@Autowired
UserService userService;

public String sendMail() {
ZoneId zoneId = userService.zoneId;
System.out.println(zoneId); // null 用final标注的成员变量为null?
...
}
}

Spring使用CGLIB生成Proxy的原理

第一步,正常创建一个UserService的原始实例,这是通过反射调用构造方法实现的,它的行为和我们预期的完全一致;

第二步,通过CGLIB创建一个UserService的子类,并引用了原始实例和LoggingAspect

1
2
3
4
5
6
7
8
9
10
11
12
public class UserService$$EnhancerBySpringCGLIB extends UserService {
UserService target;
LoggingAspect aspect;

public UserService$$EnhancerBySpringCGLIB() {
}

public ZoneId getZoneId() {
aspect.doAccessCheck();
return target.getZoneId();
}
}

为了让调用方获得UserService的引用,它必须继承自UserService。然后,该代理类会覆写所有publicprotected方法,并在内部将调用委托给原始的UserService实例。

出现两个UserService实例:

原始实例 和 原始实例的代理类实例

那么最终的问题来了:proxy实例的成员变量,也就是从UserService继承的zoneId,它的值是null

原因在于,UserService成员变量的初始化:

1
2
3
4
public class UserService {
public final ZoneId zoneId = ZoneId.systemDefault();
...
}

UserService$$EnhancerBySpringCGLIB中,并未执行。原因是,没必要初始化proxy的成员变量,因为proxy的目的是代理方法。

实际上,成员变量的初始化是在构造方法中完成的。这是我们看到的代码:

1
2
3
4
5
6
7
public class UserService {
public final ZoneId zoneId;
public UserService() {
super(); // 构造方法的第一行代码总是调用super()
zoneId = ZoneId.systemDefault(); // 继续初始化成员变量
}
}

然而,对于Spring通过CGLIB动态创建的UserService$$EnhancerBySpringCGLIB代理类,它的构造方法中,并未调用super(),因此,从父类继承的成员变量,包括final类型的成员变量,统统都没有初始化。

Java语言规定,任何类的构造方法,第一行必须调用super(),如果没有,编译器会自动加上,怎么Spring的CGLIB就可以搞特殊?

这是因为自动加super()的功能是Java编译器实现的,它发现你没加,就自动给加上,发现你加错了,就报编译错误。但实际上,如果直接构造字节码,一个类的构造方法中,不一定非要调用super()。Spring使用CGLIB构造的Proxy类,是直接生成字节码,并没有源码-编译-字节码这个步骤.

故: Spring通过CGLIB创建的代理类,不会初始化代理类自身继承的任何成员变量,包括final类型的成员变量!

启用了AOP,如何修复?

修复很简单,只需要把直接访问字段的代码,改为通过方法访问:

1
2
3
4
5
6
7
8
9
10
11
@Component
public class MailService {
@Autowired
UserService userService;

public String sendMail() {
// 不要直接访问UserService的成员变量,改用方法间接访问;
ZoneId zoneId = userService.getZoneId();
...
}
}

无论注入的UserService是原始实例还是代理实例,getZoneId()都能正常工作,因为代理类会覆写getZoneId()方法,并将其委托给原始实例:

1
2
3
4
5
6
7
8
public class UserService$$EnhancerBySpringCGLIB extends UserService {
UserService target = ...
...

public ZoneId getZoneId() {
return target.getZoneId();
}
}

注意到我们还给UserService添加了一个public+final的方法:

1
2
3
4
5
6
7
8
9
10
@Component
public class UserService {
...
public final ZoneId getFinalZoneId() {
return zoneId;
}
}

10:43:09.929 [main] DEBUG org.springframework.aop.framework.CglibAopProxy - Final method [public final java.time.ZoneId xxx.UserService.getFinalZoneId()] cannot get proxied via CGLIB: Calls to this method will NOT be routed to the target instance and might lead to NPEs against uninitialized fields in the proxy instance.
因为被代理的UserService有一个final方法getFinalZoneId(),这会导致其他Bean如果调用此方法,无法将其代理到真正的原始实例,从而可能发生NPE异常。

如果在MailService中,调用的不是getZoneId(),而是getFinalZoneId(),又会出现NullPointerException,这是因为,代理类无法覆写final方法(这一点绕不过JVM的ClassLoader检查),该方法返回的是代理类的zoneId字段,即null

因此,正确使用AOP,我们需要一个避坑指南:

  1. 访问被注入的Bean时,总是调用方法而非直接访问字段;
  2. 编写Bean时,如果可能会被代理,就不要编写public final方法。

这样才能保证有没有AOP,代码都能正常工作。

参考

廖雪峰

数学分类

清华大学数学系本科培养方案

培养目标及培养结果

课程安排

数学基础 范畴论 集合论* 数理逻辑* 数学哲学
代数 抽象代数 初等代数* 线性代数* 多重线性代数* 泛代数
数学分析 微积分 实变函数* 复变函数* 微分方程* 泛函分析* 调和分析
离散数学 组合数学 图论* 序理论* 博弈论
几何 代数几何 解析几何* 微分几何* 离散几何学* 欧几里得几何* 非欧几何* 有限几何学
数论 算术 代数数论* 解析数论* 几何数论* 丢番图几何
拓扑学 代数拓扑 微分拓扑* 几何拓扑
统计学 测度与概率 数理统计学* 数据科学* 统计推断* 回归分析* 统计学习理论* 机器学习* 人工智能* 数据结构与算法
计算数学 计算机科学 计算理论* 数值分析* 最优化* 计算机代数
应用数学 控制论 信息论* 计算化学* 数理生物学* 数理经济学* 计量经济学* 数理金融学* 数学心理学* 数学物理学
  • © 2015-2024 DXShelley
  • Powered by Hexo Theme Ayer

请我喝杯咖啡吧~~~

支付宝
微信