标签:mon public alt factor jar包 空间 rmi apache 字母
本系列笔记均是对b站教程https://www.bilibili.com/video/av47952931 的学习笔记,非本人原创
注解配置和基于xml的配置功能是一样的,只是配置形式不一样
这里以一个项目为例,项目还是之前的那个
AccountDAOImpl:
package com.jiading.dao.impl;
import com.jiading.dao.IAccountDAO;
import org.springframework.stereotype.Repository;
@Repository
public class AccountDAOImpl implements IAccountDAO {
/*
模拟保存
*/
public void saveAccount() {
System.out.println("保存了账户");
}
}
AcccountServiceImpl:
package com.jiading.service.impl;
import com.jiading.dao.IAccountDAO;
import com.jiading.dao.impl.AccountDAOImpl;
import com.jiading.service.IAccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/*
曾经xml的配置:
<bean id="accountService" class="com.jiading.service.impl.AccountServiceImpl"></bean>
用于创建对象的注解
作用和xml中编写bean标签实现的功能是一样的
@Component:将当前对象存入spring容器中
属性:
value:指定bean的id,不写时默认值是当前类名且首字母小写
@Controller:一般用在表现层
@Service:一般用在业务层
@Repository:一般用于持久层
以上三个注解的作用和属性,和Component一模一样
他们三个是spring框架为我们提供明确的三层使用的注解,使我们的三层框架更加清晰
用于注入数据的
作用和bean标签下写<property>作用一样,只能注入bean类型的,基本类型和String类型不能用此
@Autowired:自动按照类型注入,只要容器中有唯一的bean对象类型和要注入的变量类型匹配,就可以注入成功
出现位置:可以是变量上,也可以是方法上
细节:在使用注解注入时,set方法就不是必须的了
@qualifier:在按照类中注入的基础上再按照名称注入。它在给类成员注入时不能单独使用,但是给方法的参数注入时可以
属性:
value:用于指定注入bean的id
@Resource:直接按照bean的id注入,可以独立使用
属性:name,用于指定bean的id
集合类型的注入只能通过xml实现
对于基本类型和String类型:
@Value:注入基本类型和String类型的数据
属性:
value,用于指定数据的值,也可以使用spring中的spEl,也即spring中的El表达式
SpEl的写法:$(表达式)
用于改变作用范围的
作用和bean标签中使用scope一样
@scope:用于指定bean的作用范围
属性:
value,指定范围的取值。常用值也是singleton和prototype
和生命周期相关的
作用和bean标签中使用init-method和destory-method一样
了解即可
@PreDestory:用于指定销毁方法
@PostCOnstruct:用于指定初始化方法
这两个注解都是加在方法前面的
*/
@Repository
@Scope("singleton")
public class AccountServiceImpl implements IAccountService {
@Autowired
@Qualifier("accountDAOImpl")
//就像这样,在给类成员注解时必须和autiwired一起
//上面那两个和@Resource(name="accountDAO")等价
//@Autowired:自动按照类型注入,只要容器中有唯一的bean对象类型和要注入的变量类型匹配,就可以注入成功。
// 如果没有就报错,有多个匹配结果默认也报错,除非有对象的id和这里的变量名一样,也就是这里的变量叫accountDAO,有没有一个容器中的bean的id(即注解时候Key)的值)也叫accountDAO
private IAccountDAO accountDAO ;//spring从容器中找符合类型要求的,像这里虽然是个接口,但是它的实现类也是符合要求的,所以就用实现类了
@PostConstruct
public void init(){
System.out.println("初始化方法执行了");
}
@PreDestroy
public void destory(){
System.out.println("销毁方法执行了");
}
public void saveAccount() {
accountDAO.saveAccount();
}
}
Client:
package com.jiading.ui;
import com.jiading.dao.IAccountDAO;
import com.jiading.dao.impl.AccountDAOImpl;
import com.jiading.service.IAccountService;
import com.jiading.service.impl.AccountServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Client {
/*
获取spring的IOC核心容器,并根据ID获取对象
*/
public static void main(String[] args) {
//1. 获取核心容器对象
ClassPathXmlApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
//2.根据id获取bean对象
IAccountService as=(IAccountService)ac.getBean("accountServiceImpl");
System.out.println(as);
AccountDAOImpl acDAO = ac.getBean("accountDAOImpl", AccountDAOImpl.class);
System.out.println(acDAO);
as.saveAccount();
ac.close();//只有主动close了容器,才能调用bean的销毁方法
}
}
bean.xml:
是的,虽然使用的是注解,xml还是要配置下的,只是配置下创建容器时要扫描的包
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 告知spring在创建容器时要扫描的包,从而找到其中的注解
context名称空间和约束中-->
<!-- 扫描base-package下所有的包-->
<context:component-scan base-package="com.jiading"></context:component-scan>
</beans>
注意,使用注解配置也不是完全排除xml的配置方法,如果我们要引用一些外部资源的话,依然可以在xml中配置的,然后使用时使用@Autowired来使用
先介绍一下什么是dbUtils:
来源: https://www.cnblogs.com/CQY1183344265/p/5854418.html
Dbutils:主要是封装了JDBC的代码,简化dao层的操作。
作用:帮助java程序员,开发Dao层代码的简单框架。
框架的作用:帮助程序员,提高程序的开发效率。
出生:Dbutils是由Apache公司提供。
还有c3p0是什么:
https://baike.baidu.com/item/c3p0
C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate、Spring等。
这个项目有很多可以借鉴的地方
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>day02_ioc_account_xml</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>
</dependencies>
</project>
关键看bean.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置数据源 -->
<bean id="dataSoure" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/jd_learning"></property>
<property name="user" value="root"></property>
<property name="password" value="<密码>"></property>
</bean>
<!-- queryRunner不能是单例对象,防止多线程出现问题-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!-- 注入数据源 -->
<constructor-arg name="ds" ref="dataSoure"></constructor-arg>
</bean>
<bean id="accountDao" class="com.jiading.dao.impl.AccountDaoImpl">
<property name="runner" ref="runner"></property>
</bean>
<bean id="accountService" class="com.jiading.service.impl.AccountServiceImpl">
<!-- 注入dao对象-->
<property name="accountDao" ref="accountDao"></property>
</bean>
</beans>
注意这里使用c3p0作为数据库连接对象,而dbutils的QueryRunner作为SQL命令的执行对象
每一个模块都是对其他模块有所依赖的,一般的依赖是通过在模块内创建对象的引用,然后通过set函数进行注入,例如AccountServiceImpl.java中的这段:
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
private IAccountDao accountDao;
这需要我们在配置文件中就将依赖配置好(当然用注解的话就是在类中配置了),例如:
<bean id="accountService" class="com.jiading.service.impl.AccountServiceImpl">
<!-- 注入dao对象-->
<property name="accountDao" ref="accountDao"></property>
</bean>
但是有些类没有set函数,这时可以使用构造函数配置方法,例如配置queryRunner:
<!-- queryRunner不能是单例对象,防止多线程出现问题-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!-- 注入数据源 -->
<constructor-arg name="ds" ref="dataSoure"></constructor-arg>
</bean>
对于基本类型的配置就比较简单:
<!-- 配置数据源 -->
<bean id="dataSoure" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/jd_learning"></property>
<property name="user" value="root"></property>
<property name="password" value="<密码>"></property>
</bean>
通过这种方法,各个层的类在对于依赖可以直接声明其对象,然后设计一个set函数,之后直接调用使用即可
package com.jiading.dao.impl;
import com.jiading.dao.IAccountDao;
import com.jiading.domain.Account;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import java.sql.SQLException;
import java.util.List;
public class AccountDaoImpl implements IAccountDao {
public void setRunner(QueryRunner runner) {
this.runner = runner;
}
private QueryRunner runner;
public List<Account> findAllAccount() {
try {
return runner.query("select * from account", new BeanListHandler<Account>(Account.class));
} catch (Exception e) {
throw new RuntimeException();
}
}
public Account findAccountById(Integer accountId) {
try {
return runner.query("select * from account where id=?", new BeanHandler<Account>(Account.class),accountId);
} catch (Exception e) {
throw new RuntimeException();
}
}
public void saveAccount(Account acc) {
try {
runner.update("insert into account(name,money) values(?,?)",acc.getName(),acc.getMoney());
} catch (Exception e) {
throw new RuntimeException();
}
}
public void updateAccount(Account account) {
try {
//name和money都是可变的,不变的是id,也就是主键,所以查找的时候要查id
runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
} catch (Exception e) {
throw new RuntimeException();
}
}
public void deleteAccount(Integer accountId) {
try {
runner.update("delete from account where id=?",accountId);
} catch (Exception e) {
throw new RuntimeException();
}
}
}
package com.jiading.service.impl;
import com.jiading.dao.IAccountDao;
import com.jiading.domain.Account;
import com.jiading.service.IAccountService;
import java.util.List;
public class AccountServiceImpl implements IAccountService {
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
private IAccountDao accountDao;
public List<Account> findAllAccount() {
return accountDao.findAllAccount();
}
public Account findAccountById(Integer accountId) {
return accountDao.findAccountById(accountId);
}
public void saveAccount(Account acc) {
accountDao.saveAccount(acc);
}
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
public void deleteAccount(Integer accountId) {
accountDao.deleteAccount(accountId);
}
}
还要配置数据对象Account.java:
package com.jiading.domain;
import java.io.Serializable;
/*
账户的实体类
*/
public class Account implements Serializable {
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Float getMoney() {
return money;
}
public void setMoney(Float money) {
this.money = money;
}
/*
一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才是可序列化的。因此如果要序列化某些类的对象,这些类就必须实现Serializable接口。而实际上,Serializable是一个空接口,没有什么具体内容,它的目的只是简单的标识一个类的对象可以被序列化。
? ? ? ? 什么情况下需要序列化:
? ? ? ? 1.?????当你想把的内存中的对象写入到硬盘的时候。
? ? ? ? 2.?????当你想用套接字在网络上传送对象的时候。
? ? ? ? 3.?????当你想通过RMI传输对象的时候。
*/
private Integer id;
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
private String name;
private Float money;
}
测试类:
package com.jiading.test;
import com.jiading.domain.Account;
import com.jiading.service.IAccountService;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.List;
/*
使用Junit单元测试,测试我们的配置
*/
public class AccountServiceTest {
@Test
public void testFindAll() {
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
IAccountService accountService = (IAccountService)ac.getBean("accountService");
List<Account> allAccount = accountService.findAllAccount();
for (Account account:allAccount){
System.out.println(account);
}
}
@Test
public void testFindone() {
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
IAccountService accountService = (IAccountService)ac.getBean("accountService");
Account accountById = accountService.findAccountById(1);
System.out.println(accountById);
}
@Test
public void testSave() {
Account account=new Account();
account.setName("test");
account.setMoney(12345f);
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
IAccountService accountService = (IAccountService)ac.getBean("accountService");
accountService.saveAccount(account);
}
@Test
public void testUpdate() {
}
@Test
public void testDelete() {
}
}
我们可以将上面使用xml的配置转为使用注解
这里就举一点例子吧,剩下的大同小异
@Component("accountService")
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao;
bean.xml现在是:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.jiading"></context:component-scan>
<!-- 配置数据源 -->
<bean id="dataSoure" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/jd_learning"></property>
<property name="user" value="root"></property>
<property name="password" value="<密码>"></property>
</bean>
<!-- queryRunner不能是单例对象,防止多线程出现问题-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!-- 注入数据源 -->
<constructor-arg name="ds" ref="dataSoure"></constructor-arg>
</bean>
</beans>
但是不能在创建容器之后使用:
@AutoWired
IAccountService accountService;
那么,我们能不能连这剩下的xml也不用,完全拜托这个xml文档而纯用注解呢?其实是可以的,但是改动要比较大:
我们新建一个config包,和com包并列
在测试类中,调用的方式变成了:
//获取容器
ApplicationContext ac=new AnnotationConfigApplicationContext(springConfiguration.class);
/*
使用AnnotationConfigApplicationContext创建容器时,可以指定多个配置类(这个构造函数是可变参数的),它们之间是兄弟关系
*/
这个springConfiguration就是我们的主配置类,它在config这个包里:
package config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
/*
该类是一个配置类,作用和bean.xml一样
为了达到这样的效果,需要加注解@Configuration,作用就是指定当前类是一个配置类
当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写
@ComponentScan:用于通过注解指定spring在创建容器时要扫描的包,就和xml中 <context:component-scan base-package="">的作用一样
属性写value或者basepackage都可以
*/
/*
@Import:用于导入其他的配置类
*/
@Configuration
//@ComponentScan({"com.jiading","config"})
//如果使用@Inport的话就可以改写为:
@ComponentScan("com.jiading")
@Import(JdbcConfig.class)//加了这个,在JdbcConfig中甚至就不需要加@Configuration注解了
/*
使用Import注解之后,有Import的类就是父配置类,Import指向的是子配置类
*/
@PropertySource("classpath:jdbcConfig.properties")
//@PropertySource:指定配置文件的位置
//classpath表示后面的路径是类路径
public class springConfiguration {
}
为什么它里面没有内容呢,因为我们原来留下的就是一些JDBC的相关操作,而这是比较公共的配置,可能有多个模块都要用到,所以把它独立出来,不和该模块的总体配置混在一起:
JdbcConfig.java
package config;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import javax.sql.DataSource;
/*
和spring连接数据库相关的配置类
*/
@Configuration
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
/*
用于创建一个queryrunner对象
@Bean:用于把当前方法的返回值作为bean对象,存入spring的IOC容器中
属性:
name:用于指定bean的id,默认值是当前方法的名称
@Scope:用于指定是单例还是多例,默认是单例的
*/
@Bean(name = "runner")
@Scope("prototype")
public QueryRunner createQueryRunner(DataSource datasource) {
//这里传入的参数spring会自动从容器中找,和Autowired步骤一样:自动按照类型匹配,如果类型一样的话优先参数名和id一样的
return new QueryRunner(datasource);
}
@Bean(name = "datasource")
/*
将配置参数定义在外边,随时可改,不需要修改程序和重新编译
*/
public DataSource createDataSource() {
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
return ds;
} catch (Exception e) {
throw new RuntimeException();
}
}
}
为什么这里没有直接将参数放进去,而是使用Value注解来读取,也是为了将配置文件和程序独立起来,可以修改:
jdbcConfig.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/jd_learning
jdbc.username=root
jdbc.password=<密码>
由此可以看出,为了将项目化为纯注解项目,所费的功夫其实是挺大的。而在实际运用中,是该使用注解还是xml,首先看公司的要求。如果没有要求的话,之前注解和xml混合的方式其实挺好的:jar包中的类使用xml方式,而自己写的类使用注解方式
这里可以思考一个问题,既然我们可以使用注解来注入类,那为什么在最外面的调用时,还依然要使用IAccountService accountService = (IAccountService)ac.getBean("accountService");的方式呢?这么想是没错的,我们可以使用注解来注入,但是有一个问题是,注解只能注入类变量,不能注入方法中的局部变量,所以需要将它作为类变量才能使用注解注入。
除此之外,我们能不能不手动创建容器,特别是对于Test类中,每一个test方法都需要使用
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
IAccountService accountService = (IAccountService)ac.getBean("accountService");
才能使用,能不能改进下呢?其实是可以的
我们知道,Junit方法没有main方法但是可以执行的原因是其实是有一个隐含的main方法来调用各个Test方法的(可以运行单个Test方法,但是如果点击类名的话也可以选择运行该类中所有Test方法),而我们可以利用@before注解和init方法,使得在执行Test方法之前先执行这个方法,而我们在这个方法中将容器和AccountService初始化。此时在各个Test方法中就不需要重复定义它们了。但是这依然是不能使用注解注入AccountService的,因为AccountService是类变量,初始化的时候就需要spring进行注入,而此时spring容器还没有创建,所以在init方法中依然需要accountService = (IAccountService)ac.getBean("accountService");语句。
那有没有其他的方法呢?其实是有的,就是在main方法创建的时候就把spring容器创建好,而这使用Junit原有的main方法是无法完成的,这就需要使用整合了spring框架的junit main方法。
在pom.xml中导入:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
测试类这样写:
package com.jiading.test;
import com.jiading.domain.Account;
import com.jiading.service.IAccountService;
import config.springConfiguration;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
/*
使用Junit单元测试,测试我们的配置
spring整合junit的配置
1. 导入spring整合junit的jar
2. 使用junit提供的一个注解@Runwith,把原有的main方法替换成spring提供的main方法,为我们创建容器
3. 告知spring运行器,spring和ioc创建是基于xml还是注解的,并告知位置
当使用spring5.x版本时,要求Junit的jar包必须是4.1.2及以上版本的
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = springConfiguration.class)
/*
location属性:指定xml文件的位置,表示类路径
classes:指定注解类所在位置
*/
public class AccountServiceTest {
@Autowired
private IAccountService as;
@Test
public void testFindAll() {
List<Account> allAccount = as.findAllAccount();
for (Account account:allAccount){
System.out.println(account);
}
}
@Test
public void testFindone() {
Account accountById = as.findAccountById(1);
System.out.println(accountById);
}
@Test
public void testSave() {
Account account=new Account();
account.setName("test");
account.setMoney(12345f);
as.saveAccount(account);
}
}
Spring框架4:Spring使用注解和XML配置控制反转(IOC)
标签:mon public alt factor jar包 空间 rmi apache 字母
原文地址:https://www.cnblogs.com/jiading/p/12368803.html