Spring介绍
Spring 框架是一个Java平台,它为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构,因此您可以专注于应用程序的开发。
Spring可以让您从“plain old Java objects”(POJO)中构建应用程序和通过非侵入性的POJO实现企业应用服务。此功能适用于Java SE的编程模型,全部的或部分的适应Java EE模型。
以上摘自Spring官网中文版本。
spring5框架图
图片来自Spring官网。
SpringBean的创建生命周期
我们都知道Spring的三大特点 : IOC(控制反转), DI(依赖注入)以及AOP(切面)。
控制反转: 在传统的 java SE设计模式中,需要创建一个对象,全部是依靠程序猿手动 new() 一个对象,进行应用。而spring 中则是使用容器的方式,将一个对象交给容器,容器帮你创建,初始化以及销毁。
比如说:
以前,我们创建一个Controller类,调用一个Service 类,是直接在Controller 中new一个Service,然后调用对应的方法。
现在,我们把需要创建的对象交给Spring 管理。只要能要spring 扫描到对应的类,spring的容器会帮你将对象进行创建,属性注入,初始化。将创建对象,创建什么对象的权利交给Spring容器。这就是控制反转。
依赖注入: 就是spring 在帮你创建对象后,判断对象中有哪些属性是需要spring帮你初始化的(@Atowried,@Resouce等),根据属性的类型,名称从容器中获取对象。
AOP: 这个都知道,就是在执行一个方法前或者后,先执行的一个方法。比如说 日志记录,事物管理器这些的。
BEAN的创建过程
下面说的bean的创建过程只是简单的概述下,spring源码中的肯定不会是这么简单的。
- 首先创建一个对象,我们都知道是要调用对象的构造方法,而我们的spring就需要根据对象的配置推断出构造方法来。
- 根据推断的构造方法,创建对象。
- 得到这个对象后依赖注入,反射的方式。
- 判断对象是否实现了BeanNameAware,ApplicationContextAware等接口,调用对应的回调方法并传入对应的参数。
例如:
@Component
public class SpringUtil implements ApplicationContextAware{
private static ApplicationContext ctx;
void setApplicationContext(ApplicationContext applicationContext){
ctx= applicationContext;
}
}
这个类对象创建后,就会回调setApplicationContext这个方法,前提是这个类交给了spring管理。
- Aware回调后,Spring会判断该对象中是否存在某个方法被@PostConstruct注解了,如果存在,Spring会调用当前对象的此方法(初始化前)
- 判断类是否实现了InitializingBean方法,如果实现了,代表该类实现了afterPropertiesSet方法,调用这个方法。
- 最后,Spring会判断当前对象需不需要进行AOP,如果不需要那么Bean就创建完了,如果需要进行AOP,实现BeanPostPorcessor 类。则会进行动态代理并生成一个代理对象做为Bean(初始化后)
大概流程图
简单模拟Spring创建bean
-
首先,创建一个空白的MAVEN项目,叫MySpringModel,在java目录下建立两个包,com.project.spring(放模拟spring的代码),com.project.service(自己的业务代码)。
如下图:
-
我们都知道,现在Spring都流行使用注解的方式,而不是xml的方式,所以我们模拟的时候使用的是注解的方式。创建相关的注解, @ComponentScan(扫描包路径的注解),@Component(将对象交给spring管理的注解),@Scope(作用域) ,@PostConstrust,以及依赖注入的 @Autowired
@ComponentScan:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ComponentScan {
String value() default "";
}
@Component
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component{
String value() default "";
}
@Scope
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Scope {
String value() default "";
}
@PostConstrust
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PostConstruct {
}
@Autowired
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
- 实现一个配置类.MyConfig.java
@ComponentScan("com.project.service") //扫描的包路径
public class MyConfig {
}
- 创建一个类信息的存储器 BeanDefinition,用来存储类的信息。
public class BeanDefinition {
/*
* 单列 还是多例
**/
private String scope;
private Class clazz;
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public Class getClazz() {
return clazz;
}
public void setClazz(Class clazz) {
this.clazz = clazz;
}
}
- 创建一个初始化的接口InitalitionBean
public interface InitalitionBean {
void afterPropertiesSet();
}
- 创建一个后置处理器的接口BeanPostProcess,当bean创建完成后,会调用他的afterBeanPostProcess方法。
public interface BeanPostProcess {
default Object afterBeanPostProcess(String beanName, Object bean) {
return bean;
}
default Object beforeBeanPostProcess(String beanName, Object bean) {
return bean;
}
}
-
好了现在需要用的的注解和类都已经创建了,那么我们还需要一个启动容器。在spring包下面创建一个启动类MyApplicationContext 。首先我们分析下,一个启动类至少需要什么。
a. 我的启动类的构造方法是不是也应该像Spring一样能传入一个MyConfig的配置类。b. 我的启动类是不是要能够从MyConfig配置类中获取@ComponentScan注解中获取扫描包的路径。
c. 我扫描的时候我是不是要把扫描到的类信息,封装起来,保存下来,如果我下次使用的时候,能直接使用我保存的信息。这里要创建一个BeanDefinition类用来存储(源码中也是用的BeanDefinition)。但是我扫描的类有多个是不是要用一个集合存储起来,所以要在启动类中添加一个map,所以在启动类中添加一个属性Map ,叫BeanDefinitionMap 。(源码中也是这个)
d.我扫描的时候,我这个类可能要执行初始化话后的方法,我要判断是否实现了BeanPostProcessor ,注意实现了BeanPostProcessor的类后,所有bean初始化后的阶段都要去调用实现了BeanPostProcessor的afterBeanPostProcess方法。 我们是不是要有个集合将所有实现了BeanPostProcessor的类存这个 所以我们要添加一个 beanPostProcessList 。
e. 我获取了需要加载到类信息,我是不是要将它加载到JVM中去,调用classLoader方法.
f.我是不是需要一个创建bean的方法。并且将我创建的bean 存到容器中,所以我的容器还需要一个字段存放我创建Bean,那么创建一个Map,叫singleObject(单例池)。
j. 最后我是不是要提供一个getBean方法,让我能去使用我创建的bean.
-
现在让我们按照上面的分析,来逐步实现。
a. 实现一个构造方法 ,并且
public class MySpringApplicationContext{
public MySpringApplicationContext(Class clazz) {//这里就是传入自定义的配置类。
//扫描
try {
scan(clazz);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
b. 根据配置类扫描包
public class MySpringApplicationContext{
public MySpringApplicationContext(Class clazz) {//这里就是传入自定义的配置类。
//扫描
try {
scan(clazz);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
//扫描方法
private void scan(Class clazz) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获取扫描路径
if (!clazz.isAnnotationPresent(ComponentScan.class)) {
throw new RuntimeException("未找到扫描路径");
}
//获取扫描路径
ComponentScan componentScanAnnotation = (ComponentScan) clazz.getAnnotation(ComponentScan.class);//获取配置类上的 ComponentScan
String scanPath = componentScanAnnotation.value(); //获取路径
//获取路径下所有的class文件
List<Class> classeList = getBeanClass(scanPath);
}
//根据路径获取class文件
private List<Class> getBeanClass(String scanPath) throws ClassNotFoundException {
List<Class> classeList = new ArrayList<>();
scanPath = scanPath.replace(".", "/");
URL resource = ClassLoader.getSystemResource(scanPath);
File dir = new File(resource.getFile());
//获取启动类的加载器
ClassLoader classLoader = MySpringApplicationContext.class.getClassLoader();
if (dir.isDirectory()) {
File[] files = dir.listFiles();
for (File file : files) {
String filename = file.getAbsolutePath();
if (filename.endsWith(".class")) {
String classUrl = filename.substring(filename.indexOf("com"), filename.indexOf(".class")).replace("\\", ".");
//加载到JVM中去。
Class clazz = classLoader.loadClass(classUrl);
classeList.add(clazz);
}
}
}
return classeList;
}
}
c. 获取到所有的class后,我是不是要解析class,将对应的信息分装到BeanDefinition中,并且放入到BeanDefinitionMap,以及将实现了BeanPostProcessor的类添加到BeanPostProcessorList中。所以我们在启动各类中,添加字段BeanDefinitionMap,和BeanPostProcessorList
public class MySpringApplicationContext{
private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
private List<BeanPostProcess> beanPostProcessList = new ArrayList<BeanPostProcess>();
public MySpringApplicationContext(Class clazz) {//这里就是传入自定义的配置类。
//扫描
try {
scan(clazz);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
//扫描方法
private void scan(Class clazz) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获取扫描路径
if (!clazz.isAnnotationPresent(ComponentScan.class)) {
throw new RuntimeException("未找到扫描路径");
}
//获取扫描路径
ComponentScan componentScanAnnotation = (ComponentScan) clazz.getAnnotation(ComponentScan.class);//获取配置类上的 ComponentScan
String scanPath = componentScanAnnotation.value(); //获取路径
//获取路径下所有的class文件
List<Class> classeList = getBeanClass(scanPath);
for (Class aClass : classeList) {
if (aClass.isAnnotationPresent(Component.class)) {
Component componentAnnotation = (Component) aClass.getAnnotation(Component.class);
String beanName = componentAnnotation.value();
if ("".equals(beanName)) {
beanName = aClass.getSimpleName();
String smial = beanName.substring(0, 1).toLowerCase();
beanName = smial + beanName.substring(1);
}
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setClazz(aClass);
//判断是否实现了BeanPostProcessor
if (BeanPostProcess.class.isAssignableFrom(aClass)) {
BeanPostProcess instance = (BeanPostProcess) aClass.getDeclaredConstructor().newInstance();
beanPostProcessList.add(instance);
}
//判断是否是单利还是多例
if (aClass.isAnnotationPresent(Scope.class)) {
Scope scopeAnnotation = (Scope) aClass.getAnnotation(Scope.class);
String scopeScope = scopeAnnotation.value();
if ("propoto".equals(scopeScope)) {
beanDefinition.setScope("propoto");
} else {
beanDefinition.setScope("singleton");
}
} else {
beanDefinition.setScope("singleton");
}
beanDefinitionMap.put(beanName, beanDefinition);
}
}
}
//根据路径获取class文件
private List<Class> getBeanClass(String scanPath) throws ClassNotFoundException {
List<Class> classeList = new ArrayList<>();
scanPath = scanPath.replace(".", "/");
URL resource = ClassLoader.getSystemResource(scanPath);
File dir = new File(resource.getFile());
//获取启动类的加载器
ClassLoader classLoader = MySpringApplicationContext.class.getClassLoader();
if (dir.isDirectory()) {
File[] files = dir.listFiles();
for (File file : files) {
String filename = file.getAbsolutePath();
if (filename.endsWith(".class")) {
String classUrl = filename.substring(filename.indexOf("com"), filename.indexOf(".class")).replace("\\", ".");
//加载到JVM中去。
Class clazz = classLoader.loadClass(classUrl);
classeList.add(clazz);
}
}
}
return classeList;
}
}
d. 创建bean的方法。但是我是要创建所有的bean。在我上一步中,我不是将所有的类信息都放到了BeanDefinitionMap中去了么。那么我就只要遍历这个Map,根据Map中的beanDefinition创建bean.
public class MySpringApplicationContext {
private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
private Map<String, Object> singtleObjects = new HashMap<>();
private List<BeanPostProcess> beanPostProcessList = new ArrayList<BeanPostProcess>();
public MySpringApplicationContext(Class clazz) {
//扫描
try {
scan(clazz);
//遍历所有的BedefinitionMap
doWhileByBeanDefinitionMap();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
private void scan(Class clazz) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获取扫描路径
if (!clazz.isAnnotationPresent(ComponentScan.class)) {
throw new RuntimeException("未找到扫描路径");
}
//获取扫描路径
ComponentScan componentScanAnnotation = (ComponentScan) clazz.getAnnotation(ComponentScan.class);
String scanPath = componentScanAnnotation.value();
//获取路径下所有的class文件
List<Class> classeList = getBeanClass(scanPath);
for (Class aClass : classeList) {
if (aClass.isAnnotationPresent(Component.class)) {
Component componentAnnotation = (Component) aClass.getAnnotation(Component.class);
//获取Component的值作为beanName
String beanName = componentAnnotation.value();
//如果没有值 将类名的首字母小写。
if ("".equals(beanName)) {
beanName = aClass.getSimpleName();
String smial = beanName.substring(0, 1).toLowerCase();
beanName = smial + beanName.substring(1);
}
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setClazz(aClass);
//判断是否实现了BeanPostProcessor
if (BeanPostProcess.class.isAssignableFrom(aClass)) {
BeanPostProcess instance = (BeanPostProcess) aClass.getDeclaredConstructor().newInstance();
beanPostProcessList.add(instance);
}
//判断是否是单利还是多例
if (aClass.isAnnotationPresent(Scope.class)) {
Scope scopeAnnotation = (Scope) aClass.getAnnotation(Scope.class);
String scopeScope = scopeAnnotation.value();
if ("propoto".equals(scopeScope)) {
beanDefinition.setScope("propoto");
} else {
beanDefinition.setScope("singleton");
}
} else {
beanDefinition.setScope("singleton");
}
beanDefinitionMap.put(beanName, beanDefinition);
}
}
}
private List<Class> getBeanClass(String scanPath) throws ClassNotFoundException {
List<Class> classeList = new ArrayList<>();
scanPath = scanPath.replace(".", "/");
URL resource = ClassLoader.getSystemResource(scanPath);
File dir = new File(resource.getFile());
//获取启动类的加载器
ClassLoader classLoader = MySpringApplicationContext.class.getClassLoader();
if (dir.isDirectory()) {
File[] files = dir.listFiles();
for (File file : files) {
String filename = file.getAbsolutePath();
if (filename.endsWith(".class")) {
String classUrl = filename.substring(filename.indexOf("com"), filename.indexOf(".class")).replace("\\", ".");
Class clazz = classLoader.loadClass(classUrl);
classeList.add(clazz);
}
}
}
return classeList;
}
//遍历所有的BeanDefinitionMap
private void doWhileByBeanDefinitionMap() throws IllegalAccessException, InstantiationException {
for (Map.Entry<String, BeanDefinition> stringBeanDefinitionEntry : beanDefinitionMap.entrySet()) {
String beanName = stringBeanDefinitionEntry.getKey();
Object beanObject = createBean(beanName);
singtleObjects.put(beanName, beanObject);
}
}
//创建bean
private Object createBean(String beanName) {
BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
Object beanObject = null;
try {
Class clazz = beanDefinition.getClazz();
beanObject = clazz.newInstance();
//依赖注入
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Autowired.class)) {
field.setAccessible(true);
//注意这里因为没有实现getBean方法所有先用createBean代替
//getBean实现有如下
// field.set(beanObject,getBean(field.getName()));
field.set(beanObject, createBean(field.getName()));
}
}
//初始化前
for (Method declaredMethod : clazz.getDeclaredMethods()) {
if (declaredMethod.isAnnotationPresent(PostConstruct.class)) {
declaredMethod.invoke(beanObject);
}
}
//初始化
if (beanObject instanceof InitalitionBean) {
((InitalitionBean) beanObject).afterPropertiesSet();
}
//初始化后
for (BeanPostProcess beanPostProcess : beanPostProcessList) {
beanObject = beanPostProcess.afterBeanPostProcess(beanName,beanObject);
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
if (beanObject==null) {
throw new RuntimeException("创建Bean失败");
}
return beanObject;
}
}
e. 最后创建一个getBean方法。
public class MySpringApplicationContext {
private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
private Map<String, Object> singtleObjects = new HashMap<>();
private List<BeanPostProcess> beanPostProcessList = new ArrayList<BeanPostProcess>();
public MySpringApplicationContext(Class clazz) {
//扫描
try {
scan(clazz);
doWhileByBeanDefinitionMap();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
private void scan(Class clazz) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获取扫描路径
if (!clazz.isAnnotationPresent(ComponentScan.class)) {
throw new RuntimeException("未找到扫描路径");
}
//获取扫描路径
ComponentScan componentScanAnnotation = (ComponentScan) clazz.getAnnotation(ComponentScan.class);
String scanPath = componentScanAnnotation.value();
//获取路径下所有的class文件
List<Class> classeList = getBeanClass(scanPath);
for (Class aClass : classeList) {
if (aClass.isAnnotationPresent(Component.class)) {
Component componentAnnotation = (Component) aClass.getAnnotation(Component.class);
String beanName = componentAnnotation.value();
if ("".equals(beanName)) {
beanName = aClass.getSimpleName();
String smial = beanName.substring(0, 1).toLowerCase();
beanName = smial + beanName.substring(1);
}
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setClazz(aClass);
//判断是否实现了BeanPostProcessor
if (BeanPostProcess.class.isAssignableFrom(aClass)) {
BeanPostProcess instance = (BeanPostProcess) aClass.getDeclaredConstructor().newInstance();
beanPostProcessList.add(instance);
}
//判断是否是单利还是多例
if (aClass.isAnnotationPresent(Scope.class)) {
Scope scopeAnnotation = (Scope) aClass.getAnnotation(Scope.class);
String scopeScope = scopeAnnotation.value();
if ("propoto".equals(scopeScope)) {
beanDefinition.setScope("propoto");
} else {
beanDefinition.setScope("singleton");
}
} else {
beanDefinition.setScope("singleton");
}
beanDefinitionMap.put(beanName, beanDefinition);
}
}
}
private List<Class> getBeanClass(String scanPath) throws ClassNotFoundException {
List<Class> classeList = new ArrayList<>();
scanPath = scanPath.replace(".", "/");
URL resource = ClassLoader.getSystemResource(scanPath);
File dir = new File(resource.getFile());
//获取启动类的加载器
ClassLoader classLoader = MySpringApplicationContext.class.getClassLoader();
if (dir.isDirectory()) {
File[] files = dir.listFiles();
for (File file : files) {
String filename = file.getAbsolutePath();
if (filename.endsWith(".class")) {
String classUrl = filename.substring(filename.indexOf("com"), filename.indexOf(".class")).replace("\\", ".");
Class clazz = classLoader.loadClass(classUrl);
classeList.add(clazz);
}
}
}
return classeList;
}
private void doWhileByBeanDefinitionMap() throws IllegalAccessException, InstantiationException {
for (Map.Entry<String, BeanDefinition> stringBeanDefinitionEntry : beanDefinitionMap.entrySet()) {
String beanName = stringBeanDefinitionEntry.getKey();
Object beanObject = createBean(beanName);
singtleObjects.put(beanName, beanObject);
}
}
private Object createBean(String beanName) {
BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
Object beanObject = null;
try {
Class clazz = beanDefinition.getClazz();
beanObject = clazz.newInstance();
//依赖注入
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Autowired.class)) {
field.setAccessible(true);
field.set(beanObject,getBean(field.getName()));
}
}
//初始化前
for (Method declaredMethod : clazz.getDeclaredMethods()) {
if (declaredMethod.isAnnotationPresent(PostConstruct.class)) {
declaredMethod.invoke(beanObject);
}
}
//初始化
if (beanObject instanceof InitalitionBean) {
((InitalitionBean) beanObject).afterPropertiesSet();
}
//初始化后
for (BeanPostProcess beanPostProcess : beanPostProcessList) {
beanObject = beanPostProcess.afterBeanPostProcess(beanName,beanObject);
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
if (beanObject==null) {
throw new RuntimeException("创建Bean失败");
}
return beanObject;
}
public Object getBean(String beanName) {
if (!beanDefinitionMap.containsKey(beanName)) {
throw new RuntimeException("未找到bean对象");
}
BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
if (beanDefinition.getScope().equals("singleton")) {
return singtleObjects.get(beanName);
} else {
return createBean(beanName);
}
}
}
以上简单的spring就手写好了,现在我们测试一下。
- 创建一个Test类
public class Test {
public static void main(String[] args) {
MySpringApplicationContext app = new MySpringApplicationContext(MyConfig.class);
UserService userService = (UserService) app.getBean("userService");
userService.test();
userService.orderServiceTest();
OrderService orderService = (OrderService) app.getBean("orderService");
System.out.println(orderService.getName());
}
}
- 创建一个 UserService和OrderService,并且给UserService注入OrderService.
UserService:
@Component
public class UserService implements InitalitionBean {
@Autowired
private OrderService orderService;
@PostConstruct
public void test(){
System.out.println("UserService初始化前");
}
public void orderServiceTest(){
orderService.test();
}
@Override
public void afterPropertiesSet() {
System.out.println("UserService初始化方法");
}
}
@Component
public class OrderService {
public void test(){
System.out.println("orderService创建成功,正常调用");
}
}
- 创建一个初始化后置器MyBeanPostProcessor
@Component
public class MyBeanPostProcessor implements BeanPostProcess {
@Override
public Object afterBeanPostProcess(String beanName, Object bean) {
System.out.println(beanName+"初始化后");
return bean;
}
}
- 当然还有我们的配置类 MyConfig
@ComponentScan("com.project.service")
public class MyConfig {
}
- 最后我们执行下Test.main方法,结果如下 。可以看到我们的代码是没有问题的,而且我们最终的目录如下。
OK!! 搞定 ^ _ ^
Yout con’t live for other people. You have to be true to yourself .
忠于自己,不必未别人而活。
———摘自有道翻译