spring 注入list对象JdbcTemplate 查询的list集合数据怎样转为对象

跟我学Spring3(7.3):对JDBC的支持之关系数据库操作对象化 - ImportNew
| 标签: ,
7.3.1 概述
所谓关系数据库对象化其实就是用面向对象方式表示关系数据库操作,从而可以复用。
Spring JDBC框架将数据库操作封装为一个RdbmsOperation,该对象是线程安全的、可复用的对象,是所有数据库对象的父类。而SqlOperation继承了RdbmsOperation,代表了数据库SQL操作,如select、update、call等,如图7-4所示。
图7-4 关系数据库操作对象化支持类
数据库操作对象化只要有以下几种类型,所以类型是线程安全及可复用的:
查询:将数据库操作select封装为对象,查询操作的基类是SqlQuery,所有查询都可以使用该类表示,Spring JDBC还提供了一些更容易使用的MappingSqlQueryWithParameters和MappingSqlQuery用于将结果集映射为Java对象,查询对象类还提供了两个扩展UpdatableSqlQuery和SqlFunction;
更新:即增删改操作,将数据库操作insert 、update、delete封装为对象,增删改基类是SqlUpdate,当然还提供了BatchSqlUpdate用于批处理;
存储过程及函数:将存储过程及函数调用封装为对象,基类是SqlCall类,提供了StoredProcedure实现。
7.3.2 查询
1)SqlQuery:需要覆盖如下方法来定义一个RowMapper,其中parameters参数表示命名参数或占位符参数值列表,而context是由用户传入的上下文数据。
java代码:
RowMapper&T& newRowMapper(Object[] parameters, Map context)
SqlQuery提供两类方法:
execute及executeByNamedParam方法:用于查询多行数据,其中executeByNamedParam用于支持命名参数绑定参数;
findObject及findObjectByNamedParam方法:用于查询单行数据,其中findObjectByNamedParam用于支持命名参数绑定。
演示一下SqlQuery如何使用:
java代码:
public void testSqlQuery() {
SqlQuery query = new UserModelSqlQuery(jdbcTemplate);
List&UserModel& result = query.execute(&name5&);
Assert.assertEquals(0, result.size());
从测试代码可以SqlQuery使用非常简单,创建SqlQuery实现对象,然后调用相应的方法即可,接下来看一下SqlQuery实现:
java代码:
package cn.javass.spring.chapter7;
//省略import
public class UserModelSqlQuery extends SqlQuery&UserModel& {
public UserModelSqlQuery(JdbcTemplate jdbcTemplate) {
//super.setDataSource(jdbcTemplate.getDataSource());
super.setJdbcTemplate(jdbcTemplate);
super.setSql(&select * from test where name=?&);
super.declareParameter(new SqlParameter(Types.VARCHAR));
compile();
protected RowMapper&UserModel& newRowMapper(Object[] parameters, Map context) {
return new UserRowMapper();
从测试代码可以看出,具体步骤如下:
一、setJdbcTemplate/ setDataSource:首先设置数据源或JdbcTemplate;
二、setSql(“select * from test where name=?”):定义sql语句,所以定义的sql语句都将被编译为PreparedStatement;
三、declareParameter(new SqlParameter(Types.VARCHAR)):对PreparedStatement参数描述,使用SqlParameter来描述参数类型,支持命名参数、占位符描述;
对于命名参数可以使用如new SqlParameter(“name”, Types.VARCHAR)描述;注意占位符参数描述必须按占位符参数列表的顺序进行描述;
四、编译:可选,当执行相应查询方法时会自动编译,用于将sql编译为PreparedStatement,对于编译的SqlQuery不能再对参数进行描述了。
五、以上步骤是不可变的,必须按顺序执行。
2)MappingSqlQuery:用于简化SqlQuery中RowMapper创建,可以直接在实现mapRow(ResultSet rs, int rowNum)来将行数据映射为需要的形式;
MappingSqlQuery所有查询方法完全继承于SqlQuery。
演示一下MappingSqlQuery如何使用:
java代码:
public void testMappingSqlQuery() {
jdbcTemplate.update(&insert into test(name) values('name5')&);
SqlQuery&UserModel& query = new UserModelMappingSqlQuery(jdbcTemplate);
Map&String, Object& paramMap = new HashMap&String, Object&();
paramMap.put(&name&, &name5&);
UserModel result = query.findObjectByNamedParam(paramMap);
Assert.assertNotNull(result);
MappingSqlQuery使用和SqlQuery完全一样,创建MappingSqlQuery实现对象,然后调用相应的方法即可,接下来看一下MappingSqlQuery实现,findObjectByNamedParam方法用于执行命名参数查询:
java代码:
package cn.javass.spring.chapter7;
//省略import
public class UserModelMappingSqlQuery extends MappingSqlQuery&UserModel& {
public UserModelMappingSqlQuery(JdbcTemplate jdbcTemplate) {
super.setDataSource(jdbcTemplate.getDataSource());
super.setSql(&select * from test where name=:name&);
super.declareParameter(new SqlParameter(&name&, Types.VARCHAR));
compile();
protected UserModel mapRow(ResultSet rs, int rowNum) throws SQLException {
UserModel model = new UserModel();
model.setId(rs.getInt(&id&));
model.setMyName(rs.getString(&name&));
和SqlQuery唯一不同的是使用mapRow来讲每行数据转换为需要的形式,其他地方完全一样。
1) UpdatableSqlQuery:提供可更新结果集查询支持,子类实现updateRow(ResultSet rs, int rowNum, Map context)对结果集进行更新。
2) GenericSqlQuery:提供setRowMapperClass(Class rowMapperClass)方法用于指定RowMapper实现,在此就不演示了。具体请参考testGenericSqlQuery()方法。
3) SqlFunction:SQL“函数”包装器,用于支持那些返回单行结果集的查询。该类主要用于返回单行单列结果集。
java代码:
public void testSqlFunction() {
jdbcTemplate.update(&insert into test(name) values('name5')&);
String countSql = &select count(*) from test&;
SqlFunction&Integer& sqlFunction1 = new SqlFunction&Integer&(jdbcTemplate.getDataSource(), countSql);
Assert.assertEquals(1, sqlFunction1.run());
String selectSql = &select name from test where name=?&;
SqlFunction&String& sqlFunction2 = new SqlFunction&String&(jdbcTemplate.getDataSource(), selectSql);
sqlFunction2.declareParameter(new SqlParameter(Types.VARCHAR));
String name = (String) sqlFunction2.runGeneric(new Object[] {&name5&});
Assert.assertEquals(&name5&, name);
如代码所示,SqlFunction初始化时需要DataSource和相应的sql语句,如果有参数需要使用declareParameter对参数类型进行描述;run方法默认返回int型,当然也可以使用runGeneric返回其他类型,如String等。
7.3.3 更新
SqlUpdate类用于支持数据库更新操作,即增删改(insert、delete、update)操作,该方法类似于SqlQuery,只是职责不一样。
SqlUpdate提供了update及updateByNamedParam方法用于数据库更新操作,其中updateByNamedParam用于命名参数类型更新。
演示一下SqlUpdate如何使用:
java代码:
package cn.javass.spring.chapter7;
//省略import
public class InsertUserModel extends SqlUpdate {
public InsertUserModel(JdbcTemplate jdbcTemplate) {
super.setJdbcTemplate(jdbcTemplate);
super.setSql(&insert into test(name) values(?)&);
super.declareParameter(new SqlParameter(Types.VARCHAR));
compile();
java代码:
public void testSqlUpdate() {
SqlUpdate insert = new InsertUserModel(jdbcTemplate);
insert.update(&name5&);
String updateSql = &update test set name=? where name=?&;
SqlUpdate update = new SqlUpdate(jdbcTemplate.getDataSource(), updateSql, new int[]{Types.VARCHAR, Types.VARCHAR});
update.update(&name6&, &name5&);
String deleteSql = &delete from test where name=:name&;
SqlUpdate delete = new SqlUpdate(jdbcTemplate.getDataSource(), deleteSql, new int[]{Types.VARCHAR});
Map&String, Object& paramMap = new HashMap&String, Object&();
paramMap.put(&name&, &name5&);
delete.updateByNamedParam(paramMap);
InsertUserModel类实现类似于SqlQuery实现,用于执行数据库插入操作,SqlUpdate还提供一种更简洁的构造器SqlUpdate(DataSource ds, String sql, int[] types),其中types用于指定占位符或命名参数类型;SqlUpdate还支持命名参数,使用updateByNamedParam方法来进行命名参数操作。
7.3.4 存储过程及函数
StoredProcedure用于支持存储过程及函数,该类的使用同样类似于SqlQuery。
StoredProcedure提供execute方法用于执行存储过程及函数。
一、StoredProcedure如何调用自定义函数:
java代码:
public void testStoredProcedure1() {
StoredProcedure lengthFunction = new HsqldbLengthFunction(jdbcTemplate);
Map&String,Object& outValues = lengthFunction.execute(&test&);
Assert.assertEquals(4, outValues.get(&result&));
StoredProcedure使用非常简单,定义StoredProcedure实现HsqldbLengthFunction,并调用execute方法执行即可,接下来看一下HsqldbLengthFunction实现:
java代码:
package cn.javass.spring.chapter7;
//省略import
public class HsqldbLengthFunction extends StoredProcedure {
public HsqldbLengthFunction(JdbcTemplate jdbcTemplate) {
super.setJdbcTemplate(jdbcTemplate);
super.setSql(&FUNCTION_TEST&);
super.declareParameter(
new SqlReturnResultSet(&result&, new ResultSetExtractor&Integer&() {
public Integer extractData(ResultSet rs) throws SQLException, DataAccessException {
while(rs.next()) {
return rs.getInt(1);
super.declareParameter(new SqlParameter(&str&, Types.VARCHAR));
compile();
StoredProcedure自定义函数使用类似于SqlQuery,首先设置数据源或JdbcTemplate对象,其次定义自定义函数,然后使用declareParameter进行参数描述,最后调用compile(可选)编译自定义函数。
接下来看一下mysql自定义函数如何使用:
java代码:
public void testStoredProcedure2() {
JdbcTemplate mysqlJdbcTemplate = new JdbcTemplate(getMysqlDataSource());
String createFunctionSql =
&CREATE FUNCTION FUNCTION_TEST(str VARCHAR(100)) & +
&returns INT return LENGTH(str)&;
String dropFunctionSql = &DROP FUNCTION IF EXISTS FUNCTION_TEST&;
mysqlJdbcTemplate.update(dropFunctionSql);
mysqlJdbcTemplate.update(createFunctionSql);
StoredProcedure lengthFunction = new MysqlLengthFunction(mysqlJdbcTemplate);
Map&String,Object& outValues = lengthFunction.execute(&test&);
Assert.assertEquals(4, outValues.get(&result&));
MysqlLengthFunction自定义函数使用与HsqldbLengthFunction使用完全一样,只是内部实现稍有差别:
java代码:
package cn.javass.spring.chapter7;
//省略import
public class MysqlLengthFunction extends StoredProcedure {
public MysqlLengthFunction(JdbcTemplate jdbcTemplate) {
super.setJdbcTemplate(jdbcTemplate);
super.setSql(&FUNCTION_TEST&);
super.setFunction(true);
super.declareParameter(new SqlOutParameter(&result&, Types.INTEGER));
super.declareParameter(new SqlParameter(&str&, Types.VARCHAR));
compile();
MysqlLengthFunction与HsqldbLengthFunction实现不同的地方有两点:
setFunction(true):表示是自定义函数调用,即编译后的sql为{?= call …}形式;如果使用hsqldb不能设置为true,因为在hsqldb中{?= call …}和{call …}含义一样;
declareParameter(new SqlOutParameter(“result”, Types.INTEGER)):将自定义函数返回值类型直接描述为Types.INTEGER;SqlOutParameter必须指定name,而不用使用SqlReturnResultSet首先获取结果集,然后再从结果集获取返回值,这是mysql与hsqldb的区别;
一、StoredProcedure如何调用存储过程:
java代码:
public void testStoredProcedure3() {
StoredProcedure procedure = new HsqldbTestProcedure(jdbcTemplate);
Map&String,Object& outValues = procedure.execute(&test&);
Assert.assertEquals(0, outValues.get(&outId&));
Assert.assertEquals(&Hello,test&, outValues.get(&inOutName&));
StoredProcedure存储过程实现HsqldbTestProcedure调用与HsqldbLengthFunction调用完全一样,不同的是在实现时,参数描述稍有不同:
java代码:
package cn.javass.spring.chapter7;
//省略import
public class HsqldbTestProcedure extends StoredProcedure {
public HsqldbTestProcedure(JdbcTemplate jdbcTemplate) {
super.setJdbcTemplate(jdbcTemplate);
super.setSql(&PROCEDURE_TEST&);
super.declareParameter(new SqlInOutParameter(&inOutName&, Types.VARCHAR));
super.declareParameter(new SqlOutParameter(&outId&, Types.INTEGER));
compile();
declareParameter:使用SqlInOutParameter描述INOUT类型参数,使用SqlOutParameter描述OUT类型参数,必须按顺序定义,不能颠倒。
你这明显有问题,你这开辟了5个线程,每个线程都执行50万次操作,就等于=250000...
关于ImportNew
ImportNew 专注于 Java 技术分享。于日 11:11正式上线。是的,这是一个很特别的时刻 :)
ImportNew 由两个 Java 关键字 import 和 new 组成,意指:Java 开发者学习新知识的网站。 import 可认为是学习和吸收, new 则可认为是新知识、新技术圈子和新朋友……
新浪微博:
推荐微信号
反馈建议:@
广告与商务合作QQ:
– 好的话题、有启发的回复、值得信赖的圈子
– 写了文章?看干货?去头条!
– 为IT单身男女服务的征婚传播平台
– 优秀的工具资源导航
– 活跃 & 专业的翻译小组
– 国内外的精选博客文章
– UI,网页,交互和用户体验
– JavaScript, HTML5, CSS
– 专注Android技术分享
– 专注iOS技术分享
– 专注Java技术分享
– 专注Python技术分享
& 2017 ImportNewSpring AOP根据JdbcTemplate方法名动态设置数据源
- ITeye技术网站
博客分类:
Spring AOP根据JdbcTemplate方法名动态设置数据源
说明:现在的场景是,采用MySQL Replication的方式在两台不同服务器部署并配置主从(Master-Slave)复制;
并需要程序上的数据操作方法访问不同的数据库,比如,update*方法访问主数据库服务器,query*方法访问从数据库服务器,从而减轻读写操作数据库的压力。即把“增删改”和“查”分开访问两台服务器,当然两台服务器的数据库同步事先已经配置好。
然而程序是早已完成的使用Spring JdbcTemplate的架构,如何在不修改任何源代码的情况下达到此功能呢?
1.目前有两个数据源需要配置到Spring框架中,如何统一管理这两个数据源?
JdbcTemplate有很多数据库操作方法,关键的可以分为以下几类(使用简明通配符):execute(args..)、update(args..)、batchUpdate(args..)、query*(args..)
2.如何根据这些方法名来使用不同的数据源呢?
3.多数据源的事务管理(暂未处理)
Spring配置文件applicationContext.xml(包含相关bean类的代码)
1.数据源配置(省略了更为详细的连接参数设置):
&bean id="masterDataSource"
class="mons.dbcp.BasicDataSource"
destroy-method="close"&
&property name="driverClassName"
value="${jdbc.driverClassName}" /&
&property name="url" value="${jdbc.url}" /&
&property name="username" value="${jdbc.username}" /&
&property name="password" value="${jdbc.password}" /&
&property name="poolPreparedStatements" value="true" /&
&property name="defaultAutoCommit" value="true" /&
&bean id="slaveDataSource"
class="mons.dbcp.BasicDataSource"
destroy-method="close"&
&property name="driverClassName"
value="${jdbc.driverClassName}" /&
&property name="url" value="${jdbc.url2}" /&
&property name="username" value="${jdbc.username}" /&
&property name="password" value="${jdbc.password}" /&
&property name="poolPreparedStatements" value="true" /&
&property name="defaultAutoCommit" value="true" /&
&bean id="dataSource"
class="test.my.serivce.ds.DynamicDataSource"&
&property name="targetDataSources"&
&entry key="master" value-ref="masterDataSource" /&
&entry key="slave" value-ref="slaveDataSource" /&
&/property&
&property name="defaultTargetDataSource" ref="masterDataSource" /&
首先定义两个数据源(连接地址及用户名等数据存放在properties属性文件中),Spring可以设置多个数据源,究其根本也不过是一个普通bean罢了。
关键是ID为“dataSource”的这个bean的设置,它是这个类“test.my.serivce.ds.DynamicDataSource”的一个实例:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataS
public class DynamicDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return CustomerContextHolder.getCustomerType();
DynamicDataSource类继承了Spring的抽象类AbstractRoutingDataSource,而AbstractRoutingDataSource本身实现了javax.sql.DataSource接口(由其父类抽象类AbstractDataSource实现),因此其实际上也是一个标准数据源的实现类。该类是Spring专为多数据源管理而增加的一个接口层,参见Spring-api-doc可知:
Abstract DataSource implementation that routes getConnection() calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context.
它根据一个数据源唯一标识key来寻找已经配置好的数据源队列,它通常是与当前线程绑定在一起的。
查看其源码,知道它还实现了Spring的初始化方法类InitializingBean,这个类只有一个方法:afterPropertiesSet(),由Spring在初始化bean完成之后调用:
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("targetDataSources is required");
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
for (Iterator it = this.targetDataSources.entrySet().iterator(); it.hasNext(); ) {
Map.Entry entry = (Map.Entry)it.next();
Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
this.resolvedDataSources.put(lookupKey, dataSource);
if (this.defaultTargetDataSource != null)
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
查看其具体实现可知,Spring将所有已经配置好的数据源存放到一个名为targetDataSources的hashMap对象中(targetDataSources属性必须设置,否则异常;defaultTargetDataSource属性可以不必设置)。只是把数据源统一存到一个map中并不能做什么,关键是它还重写了javax.sql.DataSource的getConnection()方法,该方法无论你在何时使用数据库操作相关的方法时都会使用到,即使ibatis、hibernate、JPA等进行多层封装的框架底层还是使用最普通的JDBC来实现。
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
protected DataSource determineTargetDataSource() {
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null)
dataSource = this.resolvedDefaultDataS
if (dataSource == null)
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
return dataS
protected Object resolveSpecifiedLookupKey(Object lookupKey) {
return lookupK
protected abstract Object determineCurrentLookupKey();
省略部分校验代码,这里有一个必须的关键方法:determineCurrentLookupKey,也是一个抽象的有你自己实现的方法,从这个方法返回实际要使用的数据源的key(也即在前面配置的数据源bean的ID)。从Spring-api-doc中可以看到详细说明:
Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context. Allows for arbitrary keys. The returned key needs to match the stored lookup key type.
它允许任意类型的key,但必须是跟保存到数据源hashMap中的key类型一致。我们可以在Spring配置文件中指定该类型,网上看到有仁兄使用枚举类型的,是一个有不错约束性的主意。
我们的“test.my.serivce.ds.DynamicDataSource”实现了这个方法,可见具体的数据源key是从CustomerContextHolder类中获得的,并且也是使用key与当前线程绑定的方式:
public class CustomerContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal();
public static void setCustomerType(String customerType) {
contextHolder.set(customerType);
public static String getCustomerType() {
return (String) contextHolder.get();
public static void clearCustomerType() {
contextHolder.remove();
我们也可以使用全局变量的方式来存储这个key。参见java.lang.ThreadLocal:
有一位 一针见血的指出问题来:
Why is userThreadLocal declared public? AFAIK, ThreadLocal instances are typically private static fields. Also, ThreadLocal is a generic type, it is ThreadLocal&T&. An important benefit of ThreadLocal worth mentioning (from 1.4 JVMs forward), is as an alternative to synchronization, to improve scalability in transaction-intensive environments. Classes encapsulated in ThreadLocal are automatically thread-safe in a pretty simple way, since it's clear that anything stored in ThreadLocal is not shared between threads.
ThreadLocal是线程安全的,并且不能在多线程之间共享。根据这个原理,我写了下面的小例子以便进一步理解:
public class Test {
private static ThreadLocal tl = new ThreadLocal();
public static void main(String[] args) {
tl.set("abc");
System.out.println(tl.get());
new Thread(new Runnable() {
public void run() {
System.out.println(tl.get());
}).start();
做到这里,我们已经解决了第一个问题,但似乎还没有进入正题,如何根据JdbcTemplate方法名动态设置数据源呢?
2.Spring AOP切入JdbcTemplate方法配置:
&bean id="ba" class="test.my.serivce.ds.BeforeAdvice" /&
&aop:config proxy-target-class="true"&
&aop:aspect ref="ba"&
&aop:pointcut id="update"
expression="execution(* org.springframework.jdbc.core.JdbcTemplate.update*(..)) || execution(* org.springframework.jdbc.core.JdbcTemplate.batchUpdate(..))" /&
&aop:before method="setMasterDataSource"
pointcut-ref="update" /&
&/aop:aspect&
&aop:aspect ref="ba"&
&aop:before method="setSlaveDataSource"
pointcut="execution(* org.springframework.jdbc.core.JdbcTemplate.query*(..)) || execution(* org.springframework.jdbc.core.JdbcTemplate.execute(..))" /&
&/aop:aspect&
&/aop:config&
可以看到我已经使用&aop:aspect&将JdbcTemplate的4类方法进行拦截,并使用前置通知的方式(&aop:before&)在执行这些方法之前调用其他方法,具体的AOP表达式语言的含义我就不细说了。
根据最开始的说明,分别对update操作和select操作进行拦截并调用不同的方法,这个方法到底是什么呢?
其实就是给ThreadLocal设置数据源的名字(key),以便DynamicDataSource知道到底是使用哪一个数据源。
前置方法就是调用“test.my.serivce.ds.BeforeAdvice”类的某个set方法:
public class BeforeAdvice {
public void setMasterDataSource() {
CustomerContextHolder.setCustomerType("master");
public void setSlaveDataSource() {
CustomerContextHolder.setCustomerType("slave");
当前线程就会保存下设置进去的key名称并随时可以调用。
最后再配置一个JdbcTemplate bean即可。
&bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate"&
&property name="dataSource" ref="dataSource" /&
1.在解决过程中遇到的一个问题(参考: ):
Spring异常:no matching editors or conversion strategy found
引用:Spring注入的是接口,关联的是实现类。 这里注入了实现类,所以报异常了。
2.本文主要参考的文章有:
该文还包含事务管理的配置:
该文与多数据源的设置对我有一定的启发(此外还包含测试用例):
之前做过ibatis采用ehCache和osCache做缓存的配置,这篇有点类似:
多数据源的一些实际场景分析,理论重于实际:
此外,javaeye(现为iteye)的一些文章也是有参考价值的:
EOF.最初的设想到这里变成了现实。本文讲述了“Spring AOP根据JdbcTemplate方法名动态设置数据源”的整个实现过程和一些浅显的分析。
使用这样配置后在实际使用中发现仍然有问题。比如,调用jdbcTemplate的update方法后立即调用query方法查询该条记录,或者使用以下方法:
this.jdbcTemplate.update(new PreparedStatementCreator(), keyHolder)
因为数据库复制有同步间隙,这个时间晚于程序的调用,就会出现查询不到数据的情况,实际上是数据还未同步到从服务器。期待更好的解决方案!
浏览: 275920 次
来自: 北京
http://blog.csdn.net/victory08/ ...
我的解决办法是把D:\ACRS\Projects\TAIS 下 ...
总结得相当不错,支持下。
这个必须评论下,间接的救过俺的命啊}

我要回帖

更多关于 spring 注入list对象 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信