共计 15315 个字符,预计需要花费 39 分钟才能阅读完成。
作者:小傅哥
博客:https://bugstack.cn
积淀、分享、成长,让本人和别人都能有所播种!😄
一、前言
为什么,你的代码总是糊到猪圈上?
🎙怎么办,晓得你在互联网,不晓得你在哪个大厂。晓得你在加班,不晓得你在和哪个产品辩论。晓得你在偷懒,不晓得你要摸鱼到几点。晓得你在搬砖,不晓得你在盖哪个猪圈。
当你特地辛苦披星戴月的实现着,每天、每周、每月重复性的工作时,你能取得的成长是最小,失去的回报也是少的。留着最多的汗、拿着起码的钱
可能你一冲动开始看源码,但不晓得看完的源码能用到什么中央。看设计模式,看的时候懂,但改本人的代码又下不去手。其实一方面是自身技术栈的知识面有余,另外一方面是本人储备的代码也不够。最终也就导致基本没法把一些列的常识串联起来,就像你 看了 HashMap,但也联想不到分库分表组件中的数据散列也会用到了 HashMap 中的扰动函数思维和泊松散布验证
、 看了 Spring 源码,也读不进去 Mybatis 是如何解决只定义 Dao 接口就能应用配置或者注解对数据库进行 CRUD 操作
、 看来 JDK 的动静代理,也想不到 AOP 是如何设计的
。所以成体系学习,增强技术栈常识的完整性,能力更好的用上这些学习到的编码能力。
二、指标
到本章节咱们将要从 IOC 的实现,转入到对于 AOP(Aspect Oriented Programming
) 内容的开发。在软件行业,AOP 意为:面向切面编程,通过预编译的形式和运行期间动静代理实现程序性能性能的对立保护。其实 AOP 也是 OOP 的连续,在 Spring 框架中是一个十分重要的内容,应用 AOP 能够对业务逻辑的各个局部进行隔离,从而使各模块间的业务逻辑耦合度升高,进步代码的可复用性,同时也能进步开发效率。
对于 AOP 的核心技术实现次要是动静代理的应用,就像你能够给一个接口的实现类,应用代理的形式替换掉这个实现类,应用代理类来解决你须要的逻辑。比方:
@Test
public void test_proxy_class() {IUserService userService = (IUserService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{IUserService.class}, (proxy, method, args) -> "你被代理了!");
String result = userService.queryUserInfo();
System.out.println("测试后果:" + result);
}
代理类的实现根本都大家都见过,那么有了一个根本的思路后,接下来就须要思考下怎么给办法做代理呢,而不是代理类。另外怎么去代理所有合乎某些规定的所有类中办法呢。如果能够代理掉所有类的办法,就能够做一个办法拦截器,给所有被代理的办法增加上一些自定义解决,比方打印日志、记录耗时、监控异样等。
三、计划
在把 AOP 整个切面设计交融到 Spring 前,咱们须要解决两个问题,包含:如何给合乎规定的办法做代理
, 以及做完代理办法的案例后,把类的职责拆分进去
。而这两个性能点的实现,都是以切面的思维进行设计和开发。如果不是很分明 AOP 是啥,你能够把切面了解为用刀切韭菜,一根一根切总是有点慢,那么用手( 代理
) 把韭菜捏成一把,用菜刀或者斧头这样不同的拦挡操作来解决。而程序中其实也是一样,只不过韭菜变成了办法,菜刀变成了拦挡办法。整体设计构造如下图:
- 就像你在应用 Spring 的 AOP 一样,只解决一些须要被拦挡的办法。在拦挡办法后,执行你对办法的扩大操作。
- 那么咱们就须要先来实现一个能够代理办法的 Proxy,其实代理办法次要是应用到办法拦截器类解决办法的调用
MethodInterceptor#invoke
,而不是间接应用 invoke 办法中的入参 Method method 进行method.invoke(targetObj, args)
这块是整个应用时的差别。 - 除了以上的外围性能实现,还须要应用到
org.aspectj.weaver.tools.PointcutParser
解决拦挡表达式"execution(* cn.bugstack.springframework.test.bean.IUserService.*(..))"
,有了办法代理和解决拦挡,咱们就能够实现设计出一个 AOP 的雏形了。
四、实现
1. 工程构造
![spring-12-02](https://bugstack.cn/assets/images/spring/spring-12-02.png)![spring-12-02](https://bugstack.cn/assets/images/spring/spring-12-02.png)small-spring-step-11
└── src
├── main
│ └── java
│ └── cn.bugstack.springframework
│ ├── aop
│ │ ├── aspectj
│ │ │ └── AspectJExpressionPointcut.java
│ │ ├── framework
│ │ │ ├── AopProxy.java
│ │ │ ├── Cglib2AopProxy.java
│ │ │ ├── JdkDynamicAopProxy.java
│ │ │ └── ReflectiveMethodInvocation.java
│ │ ├── AdvisedSupport.java
│ │ ├── ClassFilter.java
│ │ ├── MethodMatcher.java
│ │ ├── Pointcut.java
│ │ └── TargetSource.java
│ ├── beans
│ │ ├── factory
│ │ │ ├── config
│ │ │ │ ├── AutowireCapableBeanFactory.java
│ │ │ │ ├── BeanDefinition.java
│ │ │ │ ├── BeanFactoryPostProcessor.java
│ │ │ │ ├── BeanPostProcessor.java
│ │ │ │ ├── BeanReference.java
│ │ │ │ ├── ConfigurableBeanFactory.java
│ │ │ │ └── SingletonBeanRegistry.java
│ │ │ ├── support
│ │ │ │ ├── AbstractAutowireCapableBeanFactory.java
│ │ │ │ ├── AbstractBeanDefinitionReader.java
│ │ │ │ ├── AbstractBeanFactory.java
│ │ │ │ ├── BeanDefinitionReader.java
│ │ │ │ ├── BeanDefinitionRegistry.java
│ │ │ │ ├── CglibSubclassingInstantiationStrategy.java
│ │ │ │ ├── DefaultListableBeanFactory.java
│ │ │ │ ├── DefaultSingletonBeanRegistry.java
│ │ │ │ ├── DisposableBeanAdapter.java
│ │ │ │ ├── FactoryBeanRegistrySupport.java
│ │ │ │ ├── InstantiationStrategy.java
│ │ │ │ └── SimpleInstantiationStrategy.java
│ │ │ ├── support
│ │ │ │ └── XmlBeanDefinitionReader.java
│ │ │ ├── Aware.java
│ │ │ ├── BeanClassLoaderAware.java
│ │ │ ├── BeanFactory.java
│ │ │ ├── BeanFactoryAware.java
│ │ │ ├── BeanNameAware.java
│ │ │ ├── ConfigurableListableBeanFactory.java
│ │ │ ├── DisposableBean.java
│ │ │ ├── FactoryBean.java
│ │ │ ├── HierarchicalBeanFactory.java
│ │ │ ├── InitializingBean.java
│ │ │ └── ListableBeanFactory.java
│ │ ├── BeansException.java
│ │ ├── PropertyValue.java
│ │ └── PropertyValues.java
│ ├── context
│ │ ├── event
│ │ │ ├── AbstractApplicationEventMulticaster.java
│ │ │ ├── ApplicationContextEvent.java
│ │ │ ├── ApplicationEventMulticaster.java
│ │ │ ├── ContextClosedEvent.java
│ │ │ ├── ContextRefreshedEvent.java
│ │ │ └── SimpleApplicationEventMulticaster.java
│ │ ├── support
│ │ │ ├── AbstractApplicationContext.java
│ │ │ ├── AbstractRefreshableApplicationContext.java
│ │ │ ├── AbstractXmlApplicationContext.java
│ │ │ ├── ApplicationContextAwareProcessor.java
│ │ │ └── ClassPathXmlApplicationContext.java
│ │ ├── ApplicationContext.java
│ │ ├── ApplicationContextAware.java
│ │ ├── ApplicationEvent.java
│ │ ├── ApplicationEventPublisher.java
│ │ ├── ApplicationListener.java
│ │ └── ConfigurableApplicationContext.java
│ ├── core.io
│ │ ├── ClassPathResource.java
│ │ ├── DefaultResourceLoader.java
│ │ ├── FileSystemResource.java
│ │ ├── Resource.java
│ │ ├── ResourceLoader.java
│ │ └── UrlResource.java
│ └── utils
│ └── ClassUtils.java
└── test
└── java
└── cn.bugstack.springframework.test
├── bean
│ ├── IUserService.java
│ ├── UserService.java
│ └── UserServiceInterceptor.java
└── ApiTest.java
工程源码 : 公众号「bugstack 虫洞栈」,回复:Spring 专栏,获取残缺源码
AOP 切点表达式和应用以及基于 JDK 和 CGLIB 的动静代理类关系,如图 12-2
- 整个类关系图就是 AOP 实现外围逻辑的中央,下面局部是对于办法的匹配实现,上面从 AopProxy 开始是对于办法的代理操作。
- AspectJExpressionPointcut 的外围性能次要依赖于 aspectj 组件并解决 Pointcut、ClassFilter,、MethodMatcher 接口实现,专门用于解决类和办法的匹配过滤操作。
- AopProxy 是代理的形象对象,它的实现次要是基于 JDK 的代理和 Cglib 代理。在后面章节对于对象的实例化 CglibSubclassingInstantiationStrategy,咱们也应用过 Cglib 提供的性能。
2. 代理办法案例
在实现 AOP 的外围性能之前,咱们先做一个代理办法的案例,通过这样一个能够概括代理办法的外围全貌,能够让大家更好的了解后续拆解各个办法,设计成解耦性能的 AOP 实现过程。
单元测试
@Test
public void test_proxy_method() {// 指标对象(能够替换成任何的指标对象)
Object targetObj = new UserService();
// AOP 代理
IUserService proxy = (IUserService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), targetObj.getClass().getInterfaces(), new InvocationHandler() {
// 办法匹配器
MethodMatcher methodMatcher = new AspectJExpressionPointcut("execution(* cn.bugstack.springframework.test.bean.IUserService.*(..))");
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (methodMatcher.matches(method, targetObj.getClass())) {
// 办法拦截器
MethodInterceptor methodInterceptor = invocation -> {long start = System.currentTimeMillis();
try {return invocation.proceed();
} finally {System.out.println("监控 - Begin By AOP");
System.out.println("办法名称:" + invocation.getMethod().getName());
System.out.println("办法耗时:" + (System.currentTimeMillis() - start) + "ms");
System.out.println("监控 - End\r\n");
}
};
// 反射调用
return methodInterceptor.invoke(new ReflectiveMethodInvocation(targetObj, method, args));
}
return method.invoke(targetObj, args);
}
});
String result = proxy.queryUserInfo();
System.out.println("测试后果:" + result);
}
- 首先整个案例的指标是给一个 UserService 当成指标对象,对类中的所有办法进行拦挡增加监控信息打印解决。
- 从案例中你能够看到有代理的实现 Proxy.newProxyInstance,有办法的匹配 MethodMatcher,有反射的调用 invoke(Object proxy, Method method, Object[] args),也用用户本人拦挡办法后的操作。这样一看其实和咱们应用的 AOP 就十分相似了,只不过你在应用 AOP 的时候是框架曾经提供更好的性能,这里是把所有的外围过程给你展现进去了。
测试后果
监控 - Begin By AOP
办法名称:queryUserInfo
办法耗时:86ms
监控 - End
测试后果:小傅哥,100001,深圳
Process finished with exit code 0
- 从测试后果能够看到咱们曾经对 UserService#queryUserInfo 办法进行了拦挡监控操作,其实前面咱们实现的 AOP 就是当初体现出的后果,只不过咱们须要把这部分测试的案例解耦为更具备扩展性的各个模块实现。
拆解案例
- 拆解过程能够参考截图 12-3,咱们须要把代理对象拆解进去,因为它能够是 JDK 的实现也能够是 Cglib 的解决。
- 办法匹配器操作其实曾经是一个独自的实现类了,不过咱们还须要把传入的指标对象、办法匹配、拦挡办法,都进行对立的包装,不便内部调用时进行一个入参透传。
- 最初其实是
ReflectiveMethodInvocation
的应用,它目前曾经是实现MethodInvocation
接口的一个包装后的类,参数信息包含:调用的对象、调用的办法、调用的入参。
3. 切点表达式
定义接口
cn.bugstack.springframework.aop.Pointcut
public interface Pointcut {
/**
* Return the ClassFilter for this pointcut.
* @return the ClassFilter (never <code>null</code>)
*/
ClassFilter getClassFilter();
/**
* Return the MethodMatcher for this pointcut.
* @return the MethodMatcher (never <code>null</code>)
*/
MethodMatcher getMethodMatcher();}
- 切入点接口,定义用于获取 ClassFilter、MethodMatcher 的两个类,这两个接口获取都是切点表达式提供的内容。
cn.bugstack.springframework.aop.ClassFilter
public interface ClassFilter {
/**
* Should the pointcut apply to the given interface or target class?
* @param clazz the candidate target class
* @return whether the advice should apply to the given target class
*/
boolean matches(Class<?> clazz);
}
- 定义类匹配类,用于切点找到给定的接口和指标类。
cn.bugstack.springframework.aop.MethodMatcher
public interface MethodMatcher {
/**
* Perform static checking whether the given method matches. If this
* @return whether or not this method matches statically
*/
boolean matches(Method method, Class<?> targetClass);
}
- 办法匹配,找到表达式范畴内匹配下的指标类和办法。在上文的案例中有所体现:
methodMatcher.matches(method, targetObj.getClass())
实现切点表达式类
public class AspectJExpressionPointcut implements Pointcut, ClassFilter, MethodMatcher {private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<PointcutPrimitive>();
static {SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
}
private final PointcutExpression pointcutExpression;
public AspectJExpressionPointcut(String expression) {PointcutParser pointcutParser = PointcutParser.getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution(SUPPORTED_PRIMITIVES, this.getClass().getClassLoader());
pointcutExpression = pointcutParser.parsePointcutExpression(expression);
}
@Override
public boolean matches(Class<?> clazz) {return pointcutExpression.couldMatchJoinPointsInType(clazz);
}
@Override
public boolean matches(Method method, Class<?> targetClass) {return pointcutExpression.matchesMethodExecution(method).alwaysMatches();}
@Override
public ClassFilter getClassFilter() {return this;}
@Override
public MethodMatcher getMethodMatcher() {return this;}
}
- 切点表达式实现了 Pointcut、ClassFilter、MethodMatcher,三个接口定义办法,同时这个类次要是对 aspectj 包提供的表达式校验办法应用。
- 匹配 matches:
pointcutExpression.couldMatchJoinPointsInType(clazz)
、pointcutExpression.matchesMethodExecution(method).alwaysMatches()
,这部分内容能够独自测试验证。
匹配验证
@Test
public void test_aop() throws NoSuchMethodException {AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut("execution(* cn.bugstack.springframework.test.bean.UserService.*(..))");
Class<UserService> clazz = UserService.class;
Method method = clazz.getDeclaredMethod("queryUserInfo");
System.out.println(pointcut.matches(clazz));
System.out.println(pointcut.matches(method, clazz));
// true、true
}
- 这里独自提供进去一个匹配办法的验证测试,能够看看你拦挡的办法与对应的对象是否匹配。
4. 包装切面告诉信息
cn.bugstack.springframework.aop.AdvisedSupport
public class AdvisedSupport {
// 被代理的指标对象
private TargetSource targetSource;
// 办法拦截器
private MethodInterceptor methodInterceptor;
// 办法匹配器(查看指标办法是否合乎告诉条件)
private MethodMatcher methodMatcher;
// ...get/set
}
- AdvisedSupport,次要是用于把代理、拦挡、匹配的各项属性包装到一个类中,不便在 Proxy 实现类进行应用。这和你的业务开发中包装入参是一个情理
- TargetSource,是一个指标对象,在指标对象类中提供 Object 入参属性,以及获取指标类 TargetClass 信息。
- MethodInterceptor,是一个具体拦挡办法实现类,由用户本人实现 MethodInterceptor#invoke 办法,做具体的解决。像咱们本文的案例中是做办法监控解决
- MethodMatcher,是一个匹配办法的操作,这个对象由 AspectJExpressionPointcut 提供服务。
5. 代理形象实现(JDK&Cglib)
定义接口
cn.bugstack.springframework.aop.framework
public interface AopProxy {Object getProxy();
}
- 定义一个标准接口,用于获取代理类。因为具体实现代理的形式能够有 JDK 形式,也能够是 Cglib 形式,所以定义接口会更加方便管理实现类。
cn.bugstack.springframework.aop.framework.JdkDynamicAopProxy
public class JdkDynamicAopProxy implements AopProxy, InvocationHandler {
private final AdvisedSupport advised;
public JdkDynamicAopProxy(AdvisedSupport advised) {this.advised = advised;}
@Override
public Object getProxy() {return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), advised.getTargetSource().getTargetClass(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (advised.getMethodMatcher().matches(method, advised.getTargetSource().getTarget().getClass())) {MethodInterceptor methodInterceptor = advised.getMethodInterceptor();
return methodInterceptor.invoke(new ReflectiveMethodInvocation(advised.getTargetSource().getTarget(), method, args));
}
return method.invoke(advised.getTargetSource().getTarget(), args);
}
}
- 基于 JDK 实现的代理类,须要实现接口 AopProxy、InvocationHandler,这样就能够把代理对象 getProxy 和反射调用办法 invoke 离开解决了。
- getProxy 办法中的是代理一个对象的操作,须要提供入参 ClassLoader、AdvisedSupport、和以后这个类 this,因为这个类提供了 invoke 办法。
- invoke 办法中次要解决匹配的办法后,应用用户本人提供的办法拦挡实现,做反射调用 methodInterceptor.invoke。
- 这里还有一个 ReflectiveMethodInvocation,其余它就是一个入参的包装信息,提供了入参对象:指标对象、办法、入参。
cn.bugstack.springframework.aop.framework.Cglib2AopProxy
public class Cglib2AopProxy implements AopProxy {
private final AdvisedSupport advised;
public Cglib2AopProxy(AdvisedSupport advised) {this.advised = advised;}
@Override
public Object getProxy() {Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(advised.getTargetSource().getTarget().getClass());
enhancer.setInterfaces(advised.getTargetSource().getTargetClass());
enhancer.setCallback(new DynamicAdvisedInterceptor(advised));
return enhancer.create();}
private static class DynamicAdvisedInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {CglibMethodInvocation methodInvocation = new CglibMethodInvocation(advised.getTargetSource().getTarget(), method, objects, methodProxy);
if (advised.getMethodMatcher().matches(method, advised.getTargetSource().getTarget().getClass())) {return advised.getMethodInterceptor().invoke(methodInvocation);
}
return methodInvocation.proceed();}
}
private static class CglibMethodInvocation extends ReflectiveMethodInvocation {
@Override
public Object proceed() throws Throwable {return this.methodProxy.invoke(this.target, this.arguments);
}
}
}
- 基于 Cglib 应用 Enhancer 代理的类能够在运行期间为接口应用底层 ASM 字节码加强技术解决对象的代理对象生成,因而被代理类不须要实现任何接口。
- 对于扩大进去的用户拦挡办法,次要是在 Enhancer#setCallback 中解决,用户本人的新增的拦挡解决。这里能够看到 DynamicAdvisedInterceptor#intercept 匹配办法后做了相应的反射操作。
五、测试
1. 当时筹备
public class UserService implements IUserService {public String queryUserInfo() {
try {Thread.sleep(new Random(1).nextInt(100));
} catch (InterruptedException e) {e.printStackTrace();
}
return "小傅哥,100001,深圳";
}
public String register(String userName) {
try {Thread.sleep(new Random(1).nextInt(100));
} catch (InterruptedException e) {e.printStackTrace();
}
return "注册用户:" + userName + "success!";
}
}
- 在 UserService 中提供了 2 个不同办法,另外你还能够减少新的类来退出测试。前面咱们的测试过程,会给这个两个办法增加咱们的拦挡解决,打印办法执行耗时。
2. 自定义拦挡办法
public class UserServiceInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {long start = System.currentTimeMillis();
try {return invocation.proceed();
} finally {System.out.println("监控 - Begin By AOP");
System.out.println("办法名称:" + invocation.getMethod());
System.out.println("办法耗时:" + (System.currentTimeMillis() - start) + "ms");
System.out.println("监控 - End\r\n");
}
}
}
- 用户自定义的拦挡办法须要实现 MethodInterceptor 接口的 invoke 办法,应用形式与 Spring AOP 十分类似,也是包装 invocation.proceed() 放行,并在 finally 中增加监控信息。
3. 单元测试
@Test
public void test_dynamic() {
// 指标对象
IUserService userService = new UserService();
// 组装代理信息
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTargetSource(new TargetSource(userService));
advisedSupport.setMethodInterceptor(new UserServiceInterceptor());
advisedSupport.setMethodMatcher(new AspectJExpressionPointcut("execution(* cn.bugstack.springframework.test.bean.IUserService.*(..))"));
// 代理对象(JdkDynamicAopProxy)
IUserService proxy_jdk = (IUserService) new JdkDynamicAopProxy(advisedSupport).getProxy();
// 测试调用
System.out.println("测试后果:" + proxy_jdk.queryUserInfo());
// 代理对象(Cglib2AopProxy)
IUserService proxy_cglib = (IUserService) new Cglib2AopProxy(advisedSupport).getProxy();
// 测试调用
System.out.println("测试后果:" + proxy_cglib.register("花花"));
}
- 整个案例测试了 AOP 在于 Spring 联合前的外围代码,包含什么是指标对象、怎么组装代理信息、如何调用代理对象。
- AdvisedSupport,包装了指标对象、用户本人实现的拦挡办法以及办法匹配表达式。
- 之后就是别离调用 JdkDynamicAopProxy、Cglib2AopProxy,两个不同形式实现的代理类,看看是否能够胜利拦挡办法
测试后果
监控 - Begin By AOP
办法名称:public abstract java.lang.String cn.bugstack.springframework.test.bean.IUserService.queryUserInfo()
办法耗时:86ms
监控 - End
测试后果:小傅哥,100001,深圳
监控 - Begin By AOP
办法名称:public java.lang.String cn.bugstack.springframework.test.bean.UserService.register(java.lang.String)
办法耗时:97ms
监控 - End
测试后果:注册用户:花花 success!Process finished with exit code 0
- 如 AOP 性能定义一样,咱们能够通过这样的代理形式、办法匹配和拦挡后,在对应的指标办法下,做了拦挡操作进行监控信息打印。
六、总结
- 从本文对 Proxy#newProxyInstance、MethodInterceptor#invoke,的应用验证切面外围原理以及再把性能拆解到 Spring 框架实现中,能够看到一个貌似简单的技术其实核心内容往往没有太多,但因为须要为了满足后续更多的扩大就须要进行职责解耦和包装,通过这样设计模式的应用,以此让调用方能更加简化,本身也能够一直按需扩大。
- AOP 的性能实现目前还没有与 Spring 联合,只是对切面技术的一个具体实现,你能够先学习到如何解决代理对象、过滤办法、拦挡办法,以及应用 Cglib 和 JDK 代理的区别,其实这与的技术不只是在 Spring 框架中有所体现,在其余各类须要缩小人工硬编码的场景下,都会用到。比方 RPC、Mybatis、MQ、分布式工作
- 一些核心技术的应用上,都是具备很强的关联性的,它们也不是孤立存在的。而这个能把整个技术栈串联起来的过程,须要你来大量的学习、积攒、由点到面的铺设,能力在一个知识点的学习拓展到一个知识面和常识体系的建设。
七、系列举荐
- 《Java 面经手册》PDF,全书 417 页 11.5 万字
- 工作两三年了,整不明确架构图都画啥?
- 码农云服务应用学习,部环境、开始口、配域名、弄 SSL、搭博客!
- 毕业前写了 20 万行代码,让我从成为同学眼里的面霸!
- Thread.start(),它是怎么让线程启动的呢?-%E5%AE%83%E6%98%AF%E6%80%8E%E4%B9%88%E8%AE%A9%E7%BA%BF%E7%A8%8B%E5%90%AF%E5%8A%A8%E7%9A%84%E5%91%A2.html)