当前位置:   article > 正文

面试-框架篇_瑞吉外卖项目面试问什么

瑞吉外卖项目面试问什么

Spring

你对Spring了解多少?

Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发,比如说 Spring 支持 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)、可以很方便地对数据库进行访问、可以很方便地集成第三方组件(电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。

Spring 提供的核心功能主要是 IoC 和 AOP

IOC

IoC(Inverse of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。不过, IoC 并非 Spring 特有,在其他语言中也有应用。

IoC 容器是 Spring⽤来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注(DI)⼊。这样可以很⼤程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。 IoC 容器就像是⼀个⼯⼚⼀样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件/注解即可,完全不⽤考虑对象是如何被创建出来的。

AOP 动态代理

AOP(Aspect-Oriented Programming:⾯向切⾯编程)能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接⼝,那么Spring AOP会使⽤JDKProxy,去创建代理对象,⽽对于没有实现接⼝的对象,就⽆法使⽤ JDK Proxy 去进⾏代理了,这时候Spring AOP会使⽤基于asm框架字节流的Cglib动态代理 ,这时候Spring AOP会使⽤ Cglib ⽣成⼀个被代理对象的⼦类来作为代理。

当然你也可以使用 AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。

使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。

Spring AOP 和 AspectJ AOP 有什么区别?

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。

SpringBean得生命周期

​ (1)通过构造器创建 bean 实例(无参数构造)

​ (2)为 bean 的属性设置值和对其他 bean 引用(调用 set 方法)

​ (3)把 bean 实例传递 bean 后置处理器的方法 postProcessBeforeInitialization

​ (4)调用 bean 的初始化的方法(需要进行配置初始化的方法)

​ (5)把 bean 实例传递 bean 后置处理器的方法 postProcessAfterInitialization

​ (6)bean 可以使用了(对象获取到了)

​ (7)当容器关闭时候,调用 bean 的销毁的方法(需要进行配置销毁的方法)

Spring源码没有套路,全是技术:思路最清晰的Spring源码分析教程,最后再给你手写一个Spring,就问你顶不顶_哔哩哔哩_bilibili

初识spring

spring大致流程(初级)

UserService类--->无参的构造方法(相当于只是一个空对象)--->对象--->依赖注入(如果有@Autowaire注解的对象,会注入)-->Bean对象


如果是单例bean 

UserService类--->无参的构造方法(相当于只是一个空对象)--->对象--->依赖注入(如果有@Autowaire注解的对象,会注入)-->放入map---->Bean对象

 spring底层有一个map(单例池),getBean的时候就去寻找key为userService的值,如果没有就创建一个,如果有直接拿,所以spring是单例的


如果是多例bean就没有map,也不存在放入map这一步


bean对象和对象有什么区别?

bean和bean对象大部分情况下都指的是同一个对象,只不过是不同阶段

就好像10岁的小男孩(对象),成长为30岁的男人(bean对象)


spring大致流程(进阶)

UserService类--->无参的构造方法--->对象--->依赖注入-->初始化前(@PostConstruct)--->初始化(afterPropertiesSet)-->初始化后(AOP)-->放入Map单例池--->Bean对象

依赖注入后,对象有一个初始化过程:

什么是初始化前?

 userservice中有一个属性是admin,admin的含义是获取管理员信息,获取方式是从数据库中去,假如需要执行这个sql

select * from db_user where `is_admin` = 1

这样admin就被赋值成功,那spring怎么知道admin的赋值就是这个方法呢?给该方法上加入@PostConstruct ,就是初始化前需要执行的方法

源码大概是这样,遍历所有方法,看看有没有@Postconstruct的注解 


什么是初始化?

5、Bean的初始化是如何工作的?_哔哩哔哩_bilibili

Q:拿之前的案例,获取管理员,可以放到初始化前,也可以放在初始化的时候。初始化前的加@postconstruct注解,初始化的时候呢?

A:bean对象继承InitializingBean接口,然后重新afterPropertiesSet方法

 

 Q:spring是如何判断类继承了InitializingBean接口的呢?


bean的初始化和bean的实例化区别


什么是初始化后?

其实初始化后就是AOP,如果使用动态代理,那就好生产一个代理对象,那就不是将之前实例化的对象放入map单例池,而是将代理对象放入。后面会详细解释初始化后(AOP)

那spring的流程就会再次进阶成


进一步spring

无参的构造方法的说明

9、什么是先bytype再byName_哔哩哔哩_bilibili

s在实例化bean的时候

如果类中只定义了一个构造方法(无论是有参还是无参)那就会调用这个方法

如果类中只定义了多个构造方法,如果有无参就调用无参如果没有无参就报错

especially,我们可以指定spring调用某个构造方法,只要在那个构造方法上面加上@Autowired

就可以

 这样我们就可以把无参的构造方法改成推断构造方法

Q:那么问题来啦,调用有参构造方法的时候,spring是怎么知道参数是什么的

A:参数类型是OrderService,那spring会去map单例池中找OrderService bean对象,有的话,直接调用,没有的话就创建,那又来问题了,会出现循环依赖

Q;去map单例池中寻找对象,是根据type还是根据name?

A:如果根据name,会不会乱套,假如写错了name,写成

public Userservice(orderservice userservice) ,那spring不就去找userservice这个bean对象,不就会出错,即ByName的话,如果名字乱套,随便取名字就会出错,所以spring是ByType。

Q:但是又有问题了,Orderservice类型的bean对象有好几个怎么办,即在map单例池中,key为Orderservice的value有好多个,那怎么具体到哪一个Orderservice类型的bean对象呢?

A:spring就会现根据type找到符合条件得bean,然后再根据入参得名字去寻找bean ,如果我们得入参是OrderService orderService,那spring就找到第三个 OrderService 类型得bean

 

 注意,application.getBean是根据name调用的bean对象

 再讲一下什么是单例bean

 如果getbean按name取多次,取得都是同一个对象。如果

总结:推断构造方法!!!

 spring去类中寻找构造方法,如果有无参构造方法就选无参构造方法,如果没有无参但是只有一个有参构造方法,那就调用唯一的有参。

但是如果没有无参,且有多个有参,spring就看看哪个有参构造方法上面加了@Autowired注解,如果没有加@Autowired注解的有参,就会报错。

确定好了构造方法后,看看选定的构造方法中有没有入参,如果有入参就根据“先byType再ByName”的方式去map单例池中找到符合条件的bean对象,执行构造方法。


依赖注入的说明

 根据@Autowired注解,去单例池中找到对应的bean对象,也是根据“先byType再byName”的方法


初始化后(AOP)的说明

理论

先看一下,但目前为止,我们看到的bean创建流程,初始化后得到代理对象,这里是不会对代理对象进行DI的

简述一下动态代理

搞一个代理类继承UserService,重写test方法(需要扩展的方法),然后使用super.test()调用原来的test方法,然后在super.test()前后加上需要扩展的逻辑

但是这样存在一个问题,如果UserService注入了对象OrderService,且调用了OrderService的方法,如果super.test(),是没办法注入OrderService对象的,因为初始化后得到代理对象,spring是不会对代理对象进行DI的. spring是真没搞得呢?

是直接调用普通对象,然后调用test方法 

 

有了这个理论基础之后,我们来看看spring的事务

11、Spring事务底层是怎么工作的?_哔哩哔哩_bilibili

@Transcatinal 就是事务的注解,假如在类UserService的test方法上加了@Transcatinal

他的底层原理就是,先看看有没有@Transcatinal注解的方法,有的话就会使用AOP,在test方法前后加了切面逻辑,具体如下

这里对事务管理器建立连接的说明还不够准确,详细看下面@configuration部分

这就是aop的思想! 使用了aop的对象,就会被spring创建一个代理对象

 为了加深理解,我们再来看一下事务失效的情况

propagation是事务的传播,NEVER的意思就是,如果已经存在了事务,就抛异常,第一感觉是肯定抛异常,因为test方法已经有事务了,在调用a方法就会报错

但实际情况是不会报错,a方法上的@Transactional注解失效。

为什么会失效?

由上面AOP的理论部分可以知道spring运行时候,代理对象调用test的对象是普通对象,即普通对象中只存在普通方法,即所有注解都是失效的。 

翻译一下,就是这样

  1. @Transactional
  2. public void test(){
  3. jdbcTemplate.execute( "insert into t1 values(1,1,1,1,'1')");
  4. //a();
  5. jdbcTemplate.execute( "insert into t1 values(2,2,2,2,'2')");
  6. }
  7. //@Transactional(propagation = Propagation.NEVER)
  8. //public void a(){
  9. // jdbcTemplate.execute( "insert into t1 values(2,2,2,2,'2')");
  10. //}

那我们该怎么做到让两个注解都生效呢?

那就让a方法,也被代理对象调用 


 @Configuration

12、@Configuration注解的作用是什么?_哔哩哔哩_bilibili

 我们可以看到事务管理器获取连接的时候需要一个datasource,jdbc建立连接也需要datasource

 书接上文,Q:为什么不用JDBC进行连接?

A:多个SQL,就会多个JDBC建立连接,spring只会在增强的部分关闭自动提交和提交/回滚,多个jdbc自己建立连接,就不好统一管理;

所以spring自己建立连接,然后jdbc共享这个连接,就方便统一管理。

Q:那如何使得jdbc共享这个连接呢?

A:使用ThreadLocal,Map<DataSource,conn>的形式存放,如果jdbc和事务管理器能得到同一个datasource,那就能获取同一个连接

Q:那如何保证拿到同一个datasource呢?

A;看上面第一个图,是没有加@Configuration的,如果调用JDBC的方法,JDBC就去调用datasource方法,创建一个新连接,

如果调用事务管理器方法,调用datasource方法(无法保证datasource跟jdbc的一样),又创建一个新连接,就是不同的连接(即无法保证两次获取的datasource都是去容器里取得,我们见到的去容器中取,都是使用@Autowired等注解)。加上@Configuration,就会将Appconfig变成代理对象,前置通知就会判断是否存在datasoure,如果没有就执行蓝框子中的datasource方法,注入到容器中,这样另一个获取datasource的时候直接从容器中取,就能保证是同一个datasource


循环依赖

将类注入到容器中,spring会将类名的第一个大写字母改为小写,然后命名这个类,但是如果该类的前两个字母都是大写,那就不会转为小写

回答第一步什么是循环依赖 

假设我们有两个类,其中AService中注入了BService,BService中注入了AService

我们知道bean的生命周期中创建完了一个AService普通对象之后,就要填充BService属性,这时候就要去单例池中去找BService对象,发现没有,就去创建BService的bean对象,先是创建BService的普通对象,然后填充aService属性,这时候就要去单例池中去找AService对象,发现没有,就又去 创建AService的普通对象,进入死循环,这就是循环依赖。

回答第二步:先看看二级缓存的解决效果

 Q:普通对象不是一个空对象吗,啥也没有,如果A中有其他的加了@Autowired的属性不是空的吗,这样B怎么调用A

A:不是的,zhouyumap中存的是地址,A创建生命周期后续会填入的,那B从map中取得A也会同步。所以@Autowired并不是AOP的成分

那么这样看来好像也听不错的

但是如果这样给b的是A的普通对象,但是A放在单例池中的是代理对象。这里还是要继续详细了解一下

(猜想,@Autowired的意思是去容器中取代理对象(如果存在AOP),跟B取A的普通对象,矛盾)

那么我们在zhouyumap中直接放aop不就行了

接上面的思路,提前AOP,然后放入 zhouyumap

 Q: A怎么知道要提前AOP 呢,在图中的  ?   处可以吗?,

A:好像不可以,A还没创建B,肯定不知道B要创建A,会出现循环。只有B才会知道

下面看看三级缓存是怎么做到的

 但是如何A类中不仅有AB的循环依赖,还有AC的循环依赖怎么办?

 

 Q:为什么要搞一个二级缓存>earlysingletonobjects,不如直接放进单例池,然后C对象直接去取?

A:提前AOP获得的代理对象是不完整的,其他的属性是不完整的,如果现在就放入单例池,其他bean调用,不就会出错

所以二级缓存的作用就是存储没有经过完整的bean的声明周期的单例bean

又出现一个问题,就是在创建BService对象时,对A对象提前AOP的时候,怎么拿到A的普通对象,怎么知道AOP的操作有哪些?

 回答第三步:那么就要用到三级缓存

三个缓存的说明

一级缓存-单例池-singletonObject: 存储经过完整bean生命周期得到的bean对象

二级缓存-earlysingletonObject:存储没有经过完整的bean的生命周期的单例bean对象,能保证这类Bean是单例的

三级缓存-earlysingletonFactories:某个bean是否要出现循环依赖是不知道的,所以spring将bean的名字+beandefinition+bean对象生成lambda表达式存到三级缓存中,当出现循环依赖的时候,直接取出lambda表达式,执行,如果需要AOP就返回代理对象,如果没有就直接返回bean普通对像

注释:

①boolean issingletoncurrentlyIncreation(beanName) 这是源码中对于第0步的方法
②如果这样提前aop了,那上面的第五步就不需要aop了,且会在map中做一个判断,见图二

③如果第一步执行时候,使用的是有参构造方法,而入参里面有B类

 就不太好处理了,需要加上注解@Lazy

  1. protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  2. //先去一级缓存(单例池),找bean对象
  3. Object singletonObject = this.singletonObjects.get(beanName);
  4. //如果没有找到,并且当前正在创建当前bean
  5. if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
  6. synchronized (this.singletonObjects) {//加锁
  7. //从二级缓存中找当前bean
  8. singletonObject = this.earlySingletonObjects.get(beanName);
  9. //如果不存在,并且允许循环依赖(?)
  10. if (singletonObject == null && allowEarlyReference) {
  11. ObjectFactory<?> singletonFactory =
  12. this.singletonFactories.get(beanName);//从三级缓存中找到当前bean对象的lambda表达式
  13. if (singletonFactory != null) {
  14. //执行lambda获得代理对象或者实例对象
  15. singletonObject = singletonFactory.getObject();
  16. //存入二级缓存
  17. this.earlySingletonObjects.put(beanName, singletonObject);
  18. //删除三级缓存中的bean
  19. this.singletonFactories.remove(beanName);
  20. }
  21. }
  22. }
  23. }
  24. return singletonObject;
  25. }

 综上,终极流程


@Value

1.如果value里面的key不存在,那么就会直接将key赋值给下面的属性 

2.如果value里面不用   ${ }   这个格式的写法,那么就直接将值赋给属性


@Lazy

加在类上:

加上Lazy的类,只会在调用的时候生成bean对象

不加lazy的类,在容器创建时,也会创建

加在属性上

加上lazy的属性,没有被使用的时候,给他会赋一个代理对象,等到真正用的时候才回去容器里拿

写在方法上,就等于写在方法的所有参数上,跟写在属性上的效果是一样的



Bean的作用域

单例 :原理就是将创建好的bean对象都放入单例池中,要用直接拿,不再创建

多例:就是直接创建

Request,Session,application:都是仅用于web应用,如果作用域是request,那么会先先调用requst.getAttribute,看看能不能拿到指定的bean对象,如果有直接返回,如果没有调用requst.setAttribute,创建好了扔进去,session也是调用session.getAttribute和session.setAttribute。application调用application.getAttribute和application.setAttribute


 

SpringMVC

SpringMVC处理请求的底层原理

Tomcat启动

解析web.xml

dispatcherServlet实例化

dispatcherServlet.init()

初始化会得到spring容器-xmlwebapplication,注意此时容器还是空的

然后调用

getContextConfiguration方法,获取spring的配置文件 

然后调用图中的redresh,加载所有的bean

 SpringMVC处理请求的底层原理

1. 客户端(浏览器)发送请求,直接请求到  DispatcherServlet 。
2. DispatcherServlet 根据请求信息调⽤  HandlerMapping ,解析请求对应的  Handler 。
3. 解析到对应的  Handler (也就是  Controller 控制器)后,开始由HandlerAdapter 适配器处理。
4. HandlerAdapter 会根据  Handler 来调⽤真正的处理器开处理请求,并处理相应的业务逻辑。
5. 处理器处理完业务后,会返回⼀个  ModelAndView 对象, Model 是返回的数据对象
6. ViewResolver 会根据逻辑  View 查找实际的  View 。
7. DispaterServlet 把返回的  Model 传给  View (视图渲染)。
8. 把  View 返回给请求者(浏览器)

SpringMVC常见的注解以及作用 

@RequestMapping

将请求和处理请求的控制器方法关联 起来,建立映射关系。

@RequestParam

将@RequestParam 的value中的对应的请求参数的值赋值给后面的形参

@RequestBody

作用就是将请求体的值 赋值给它标识的形参 

@ResponseBody

用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到 浏览器,直接在浏览器显示

域对象共享数据

一共有四个域对象:

servletcontext(application):范围是服务器的开启到关闭

session:一次会话指的是浏览器的开启到关闭,跟服务器是否关闭无关

                session的钝化:浏览器没关,服务器关闭,此时会话仍在继续,原先存在session上的数据,经过序列化存放在磁盘上

                                活化:钝化后,服务器重启(此时浏览器没有关闭),将钝化的数据重新读取到session中

request:范围是一次请求,所以通过请求转发后,仍可以访问request域中的共享数据

pagecontext:仅仅在当前的页面有效

请求转发和重定向的区别

请求转发:①“一次”请求(请求是相对于浏览器,实际有两次请求):第一次发生在浏览器,第二次发生在服务器内部

                  ②地址栏不会发生变化,始终是第一次请求的地址

                  ③请求转发后仍可以获取请求域中的共享数据(用的同一个request)

                  ④请求转发可以访问WEB-INF中的资源

重定向:    ①两次请求,第一次访问servlet,第二次访问重定向的地址

                  ②地址栏发生变化,最后的地址是重定向的地址

                  ③重定向后不可以获取请求域中的共享数据

                  ④重定向后不可以访问WEB-INF中的资源

**WEB-INF中的资源只允许服务器访问,不允许浏览器直接访问

 Spring中的设计模式

面试官:“谈谈Spring中都用到了那些设计模式?”。

工厂设计模式

Spring使用工厂模式可以通过 BeanFactory 或 ApplicationContext 创建 bean 对象。

两者对比:

  • BeanFactory :延迟注入(使用到某个 bean 的时候才会注入),相比于BeanFactory来说会占用更少的内存,程序启动速度更快。

  • ApplicationContext :容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。

单例设计模式

Spring 中 bean 的默认作用域就是 singleton(单例)的。

代理设计模式

代理模式在 AOP 中的应用,详细见最上面对aop的描述

观察者模式

观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。

适配器模式

五分钟学设计模式.06.适配器模式_哔哩哔哩_bilibili

我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式

spring MVC中的适配器模式

为什么要在 Spring MVC 中使用适配器模式? Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断

装饰者模式

五分钟学设计模式.05.装饰器模式_哔哩哔哩_bilibili

装饰器模式和代理模式很像,只不过代理模式只有一个方法,是对原方法的扩展,适配器可以有多个方法,是静态的

动静态代理的区别是什么,一般在什么场景下使用_哔哩哔哩_bilibili

SpringBoot的启动流程

●SpringBoot先加载所有的自动配置类  xxxxxAutoConfiguration
●每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定
●生效的配置类就会给容器中装配很多组件
●只要容器中有这些组件,相当于这些功能就有了
●定制化配置(详细见 https://www.bilibili.com/video/BV19K4y1L7MT?p=15&spm_id_from=pageDriver 中的P15 24分钟开始)
    ○方法一:用户直接自己@Bean替换底层的组件
    ○方法二:用户去看这个组件是获取的配置文件什么值就去修改。
xxxxxAutoConfiguration ---> 组件  ---> xxxxProperties里面拿值  ----> application.properties

人话:

springboot加载所有jar包下META-INF/spring.factories文件中的自动配置类xxxAutoConfiguration,
一共127个(???为什么只加载EnableAutoConfiguration属性指定的类),但不一定127个都会被注入容器,
每一个自动配置类都会根据条件装配,按需配置。自动配置类默认都会绑定配置文件(xxxProperties),从配置文件中拿值。而这些值是从application.properties中获取的
生效的配置类就会给容器中装配很多组件,只要容器中有这些组件,相当于这些功能就有。
如果我们想修改一些默认的配置,有两个方法:
一个是直接自己@Bean替换底层的组件,一个是到自动配置类对应的配置文件对应的application.properties中修改值
 

调用run方法,run方法的执行流程是:创建SpringBoot项目时,会默认生成一个application入口类,该类中含有的main方法可以实现项目的启动,main方法中spring application的静态方法,即run方法完成对spring application的实例化操作,针对实例化对象调用另一个run方法来实现整个项目的初始化和启动。run方法的操作有:获取监听器的参数配置,打印banner信息,创建并初始化容器,监听器发送通知。

 

springe源码 第一节:基础概述--ioc如何处理_哔哩哔哩_bilibili

IOC-Ⅰ

 

 

 

 实例化和初始化

 

 

 

 BeanFactory和FactoryBean的区别

 Spring的执行流程

一第一步

第一感觉第一步应该是读取xml配置文件,然后注入到bean容器,可以bean容器还没有呢,怎么注入

所以第一步是创建bean容器

 第二步解析xml等配置文件,得到beanDefinition

第三步

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/547804
推荐阅读
相关标签
  

闽ICP备14008679号