实训笔记6.29

6.29

一、座右铭

我的故事你说,我的文字我落,我值几两你定,我去何方我挑。

二、批量数据操作

代码示例:

package com.sxuek.study;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import javax.swing.JOptionPane;

/**
 * JDBC实现批量数据的操作:仅适用于DML类型的SQL场景
 *   JDBC支持可以先将SQL语句在Java中缓存起来,小推车 addBatch(); 积攒到一定的数量级之后 统一调用executeBatch()方法将积攒
 *   的多条SQL语句传输到MySQL中执行得到结果,调用clearBatch()方法将已经执行完成的缓存SQL清空了。
 *   JDBC默认情况下连接的数据库不支持批次操作的,就算你写了批次操作,底层也是会一条一条执行的。如果你想使用批次操作,那么必须
 *   开启批次操作的开关:url后增加一个参数:rewriteBatchedStatements=true
 * 
 * 想往菜单中添加100000条数据
 * 
 * @author 11018
 *
 */
public class Demo {
    
    
	public static void main(String[] args) {
    
    
		Connection connection = null;
		PreparedStatement prepareStatement = null;
		try {
    
    
			Class.forName("com.mysql.cj.jdbc.Driver");
			connection = DriverManager.getConnection(
					"jdbc:mysql://localhost:33006/project?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true",
					"root", "123456");
			String sql = "insert into menu(menu_name,menu_price) values(?,?)";
			prepareStatement = connection.prepareStatement(sql);
			long currentTimeMillis = System.currentTimeMillis();
			for (int i = 1; i <= 100000; i++) {
    
    
				prepareStatement.setString(1, "menu"+i);
				prepareStatement.setDouble(2, 20.0);
				prepareStatement.addBatch();
				if(i % 5000 == 0) {
    
    
					int[] array = prepareStatement.executeBatch();
					prepareStatement.clearBatch();
				}
				
			}
			long time = System.currentTimeMillis();
			System.out.println(time-currentTimeMillis);

		} catch (ClassNotFoundException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (SQLException e) {
    
    
			String message = e.getMessage();
			if (message.contains("Duplicate entry")) {
    
    
				JOptionPane.showMessageDialog(null, "该菜品已经存在!");
			}
		} finally {
    
    
			if (prepareStatement != null) {
    
    
				try {
    
    
					prepareStatement.close();
				} catch (SQLException e) {
    
    
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			if (connection != null) {
    
    
				try {
    
    
					connection.close();
				} catch (SQLException e) {
    
    
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
}

三、JavaBean与数据库中的数据表

JavaBean一般是要和数据库中数据表结合使用的。

有一种特殊的JavaBean叫做实体类,实体类是专门和数据库的某张数据表对应的Java对象,用于封装数据表中对应的数据

实体类的类名===数据库的表名

实体类中属性===数据表的字段

实体类的一个对象===数据表中一行数据

实体类一般使用在如下场景:

  1. 添加/修改数据的场景 会先把零散的数据封装为实体类,然后借助实体类添加数据
  2. 查询数据的场景 会把查询回来的一行中多个列的数据封装为一个实体类的对象。

四、 JDBC实现事务操作

代码示例:

package com.sxuek.transcation;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import com.sxuek.util.DBUtils;

/**
 * JDBC实现事务操作
 *   connection 提供三个和事务有关的方法:
 *      setAutoCommit(boolean);
 *      commit()
 *      rollBack()
 *    try commit catch rollback
 *   转账操作
 * @author 11018
 *
 */
public class Demo {
    
    
	public static void main(String[] args) {
    
    
		Connection connection = null;
		PreparedStatement statement = null;
		try {
    
    
			connection = DBUtils.getConnection();
			connection.setAutoCommit(false);
			String sql ="update account set amount=amount-? where username=?";
			statement = DBUtils.getStatement(connection, sql);
			int i = DBUtils.executeDMLSQL(statement, 100,"dnn");
			String sql1 ="update account set amount=amount+? where username=?";
			statement = DBUtils.getStatement(connection, sql1);
			int i1 = DBUtils.executeDMLSQL(statement, 100,"xjq");
			connection.commit();
		} catch (SQLException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
			if(connection != null) {
    
    
				try {
    
    
					connection.rollback();
				} catch (SQLException e1) {
    
    
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
			}
		}finally {
    
    
			DBUtils.closeResources(connection, statement);
		}
		
	}
}

五、封装JDBC工具类

代码示例:

package com.sxuek.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * 封装一个JDBC的工具类
 *   目前我们操作JDBC,不管是什么样的SQL语句,执行的步骤基本上都是一摸一样的
 *   1、加载驱动
 *   2、建立连接
 *   3、准备SQL语句
 *   4、创建小推车
 *   5、执行SQL
 *   6、处理返回结果
 *   7、关闭资源
 *   
 *  工具类主要可以对JDBC做如下的封装:
 *    1、获取连接的方法封装起来
 *    2、创建小推车的方法
 *    3、执行DML类型的SQL语句以及返回结果
 *    4、执行DQL类型的SQL语句以及返回结果----最为复杂的--泛型、反射
 *    5、封装两个关闭资源的重载方法
 *    6、创建一个配置文件xxx.properties,存放数据库的相关连接信息
 * @author 11018
 *
 */
public class DBUtils {
    
    
	private static  String DRIVER;
	private static  String URL;
	private static  String USERNAME;
	private static  String PASSWORD;
	private DBUtils() {
    
    
		
	}
	/**
	 * 1、加载驱动
	 * 2、读取配置文件,把配置文件中的信息缓存起来 留备后期的静态方法调用
	 */
	static {
    
    
		Properties prop = new Properties();
		try {
    
    
			prop.load(new FileInputStream(new File("jdbc.properties")));
			DRIVER = prop.getProperty("DRIVER");
			URL = prop.getProperty("URL");
			USERNAME = prop.getProperty("USERNAME");
			PASSWORD = prop.getProperty("PASSWORD");
			Class.forName(DRIVER);
		} catch (FileNotFoundException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * 1、封装获取数据库连接的方法
	 * @throws SQLException 
	 */
	public static Connection getConnection() throws SQLException {
    
    
		Connection connection = DriverManager.getConnection(URL,USERNAME,PASSWORD);
		return connection;
	}
	
	/**
	 * 2、封装小推车的获取方法
	 * @param connection
	 * @param sql
	 * @return
	 * @throws SQLException
	 */
	public static PreparedStatement getStatement(Connection connection,String sql) throws SQLException {
    
    
		PreparedStatement prepareStatement = connection.prepareStatement(sql);
		return prepareStatement;
	}
	
	/**
	 * 3、封装一个用来执行DML类型的SQL方法
	 *   SQL语句因为使用预编译的小推车执行的,SQL语句中可能有占位符,执行SQL之前,需要传递数据把占位符给替换成我们需要的数据
	 *   
	 * @param params 参数就是用户需要给SQL的占位符替换的数据,参数有几个就证明你有几个占位符
	 * @throws SQLException 
	 */
	public static int executeDMLSQL(PreparedStatement preparedStatement,Object... params) throws SQLException {
    
    
		/**
		 * 先根据用户输入的params的个数把SQL语句的占位符替换掉
		 */
		for (int i = 0; i < params.length; i++) {
    
    
			preparedStatement.setObject(i+1, params[i]);
		}
		int executeUpdate = preparedStatement.executeUpdate();
		return executeUpdate;
	}
	
	/**
	 * 4、封装DML类型的SQL--查询单条数据的查询语句
	 *    一个只查询一条结果
	 *    查询多条结果
	 *   一般情况查询成功之后,需要把查询回来的数据封装为一个实体类的对象。
	 *   方法的返回值应该是一个实体类的对象--泛型
	 * @param clz  就是我们最后查询成功之后需要使用那个Java类来封装查询结果,Java类的运行时类
	 * @throws SQLException 
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 */
	public static <T> T executeOneResultDQLSQL(PreparedStatement preparedStatement,Class<T> clz,Object... params) throws SQLException, InstantiationException, IllegalAccessException {
    
    
		/**
		 * 先根据用户输入的params的个数把SQL语句的占位符替换掉
		 */
		for (int i = 0; i < params.length; i++) {
    
    
			preparedStatement.setObject(i+1, params[i]);
		}
		ResultSet resultSet = preparedStatement.executeQuery();
		//先根据反射机制创建这个类的对象,用来封装数据的  调用的无参构造器
		T t = clz.newInstance();
		/**
		 * 将结果集中的每一列数据封装到t对象对应的属性上
		 *   数据表的字段名=t类中的属性名   字段名是下划线命名法  属性是小驼峰命名法
		 */
		if(resultSet.next()) {
    
    
			//获取t对象中所有属性,然后将属性转换下划线的命名格式,然后在根据字段名在rs获取对应列的值,然后将值设置到t对象的对应属性中
			Field[] fields = clz.getDeclaredFields();
			for (Field field : fields) {
    
    
				field.setAccessible(true);
				//获取的小驼峰的属性名
				String name = field.getName();
				//小驼峰命名法转下划线命名法
				char[] charArray = name.toCharArray();
				StringBuilder sb = new StringBuilder();
				for (char ch : charArray) {
    
    
					if(ch >= 65 && ch <=90) {
    
    
						ch = (char)(ch+32);
						sb.append("_");
						sb.append(ch);
					}else {
    
    
						sb.append(ch);
					}
				}
				Object value = resultSet.getObject(sb.toString());
				field.set(t, value);
			}
		}
		resultSet.close();
		return t;
	}
	
	/**
	 * 5、查询多条DQL的返回结果
	 * @param <T>
	 * @param preparedStatement
	 * @param clz
	 * @param params
	 * @return
	 * @throws SQLException
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 */
	public static <T> List<T> executeMoreResultDQLSQL(PreparedStatement preparedStatement,Class<T> clz,Object... params) throws SQLException, InstantiationException, IllegalAccessException {
    
    
		/**
		 * 先根据用户输入的params的个数把SQL语句的占位符替换掉
		 */
		for (int i = 0; i < params.length; i++) {
    
    
			preparedStatement.setObject(i+1, params[i]);
		}
		ResultSet resultSet = preparedStatement.executeQuery();
		//先根据反射机制创建这个类的对象,用来封装数据的  调用的无参构造器
		List<T> list = new ArrayList<>();
		/**
		 * 将结果集中的每一列数据封装到t对象对应的属性上
		 *   数据表的字段名=t类中的属性名   字段名是下划线命名法  属性是小驼峰命名法
		 */
		while(resultSet.next()) {
    
    	
			T t = clz.newInstance();
			//获取t对象中所有属性,然后将属性转换下划线的命名格式,然后在根据字段名在rs获取对应列的值,然后将值设置到t对象的对应属性中
			Field[] fields = clz.getDeclaredFields();
			for (Field field : fields) {
    
    
				field.setAccessible(true);
				//获取的小驼峰的属性名
				String name = field.getName();
				//小驼峰命名法转下划线命名法
				char[] charArray = name.toCharArray();
				StringBuilder sb = new StringBuilder();
				for (char ch : charArray) {
    
    
					if(ch >= 65 && ch <=90) {
    
    
						ch = (char)(ch+32);
						sb.append("_");
						sb.append(ch);
					}else {
    
    
						sb.append(ch);
					}
				}
				Object value = resultSet.getObject(sb.toString());
				field.set(t, value);
			}
			//遍历得到的一条查询结果添加到集合当中
			list.add(t);
		}
		resultSet.close();
		return list;
	}
	
	/**
	 * 6、关闭资源
	 */
	public static void closeResources(Connection conn,PreparedStatement preparedStatement) {
    
    
		if (preparedStatement != null) {
    
    
			try {
    
    
				preparedStatement.close();
			} catch (SQLException e) {
    
    
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		if(conn != null) {
    
    
			try {
    
    
				conn.close();
			} catch (SQLException e) {
    
    
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

测试类:

package com.sxuek.test;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;

import com.sxuek.review.Menu;
import com.sxuek.util.DBUtils;

public class Test01 {
    
    
	public static void main(String[] args) {
    
    
		Connection connection = null;
		PreparedStatement statement = null;
		try {
    
    
			connection = DBUtils.getConnection();
			String sql = "select * from menu";
			statement = DBUtils.getStatement(connection, sql);
			//Menu menu = DBUtils.executeOneResultDQLSQL(statement, Menu.class, 1);
			List<Menu> list = DBUtils.executeMoreResultDQLSQL(statement, Menu.class);
			System.out.println(list);
		} catch (SQLException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InstantiationException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally {
    
    
			DBUtils.closeResources(connection, statement);
		}
	}
}

配置文件:jdbc

DRIVER=com.mysql.cj.jdbc.Driver
URL=jdbc:mysql://localhost:33006/project?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true
USERNAME=root
PASSWORD=123456

六、数据库连接池

数据库当中也存在一个池子的概念—数据库连接池

数据库连接池是由JDBC提供的一种思想,在JDBC操作数据库的时候,和数据库建立的连接是非常宝贵的资源,而且每一次连接的建立都得需要耗费差不多0.05~1s左右的时间

我们能不能把连接缓存起来,不用的时候放到一个地方,用的时候从那个地方获取回来一个连接使用。

JDBC提供了一个Java接口DataSource

创建数据库连接池:

1、数据库连接池的容量

2、多个数据库连接用什么东西存储—LinkedList

3、如何从数据库连接池获取一个连接

4、如果将连接放回数据库连接当中 close()

6.1 连接池

代码示例:

package com.sxuek.pool;
/**
 * JDBC常用的数据库连接池的使用:
 *   C3P0数据库连接池
 *   druid数据库连接池
 *   dbcp数据库连接池
 * @author 11018
 *
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSourceFactory;
import org.junit.Test;

import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;

public class Demo02 {
    
    
	@Test
	public void c3p0test() throws SQLException {
    
    
		/**
		 * C3P0连接池的实现类CombopooledDataSource
		 *   需要两个依赖jar包
		 * 需要创建一个配置文件 c3p0-config.xml文件 文件必须放到src目录下
		 */
		DataSource dataSource = new ComboPooledDataSource("myDataSource");
		Connection connection = dataSource.getConnection();
		System.out.println(connection);
	}
	
	@Test
	public void dbcptest() throws Exception, IOException {
    
    
		Properties prop = new Properties();
		prop.load(new FileInputStream(new File("dbcp.properties")));
		DataSource dataSource = BasicDataSourceFactory.createDataSource(prop);
		Connection connection = dataSource.getConnection();
		System.out.println(connection);
		
	}
	
	@Test
	public void druidtest() throws Exception, IOException {
    
    
		Properties prop = new Properties();
		prop.load(new FileInputStream(new File("druid.properties")));
		DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);
		Connection connection = dataSource.getConnection();
		System.out.println(connection);
		
	}
}

测试类:

package com.sxuek.pool;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

public class Test01 {
    
    
	public static void main(String[] args) {
    
    
		try {
    
    
			DataSource dataSource = new MyDataSource(2);
			Connection connection = dataSource.getConnection();
			System.out.println(connection);//com.mysql.cj.jdbc.ConnectionImpl@2aece37d
			Connection connection1 = dataSource.getConnection();
			System.out.println(connection1);//com.mysql.cj.jdbc.ConnectionImpl@548a102f
			Connection connection2 = dataSource.getConnection();
			System.out.println(connection2);//数据库连接池无可用连接,请稍后重试
			connection.close();
			Connection connection3 = dataSource.getConnection();
			System.out.println(connection3);//com.mysql.cj.jdbc.ConnectionImpl@2aece37d
			connection1.close();
			Connection connection4 = dataSource.getConnection();
			System.out.println(connection4);//com.mysql.cj.jdbc.ConnectionImpl@548a102f
		} catch (SQLException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

配置文件:

  1. c3p0

    <?xml version="1.0" encoding="UTF-8"?>
    <c3p0-config>
      <named-config name="myDataSource"> 
      	<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
      	<property name="jdbcUrl">jdbc:mysql://localhost:33006/myemployees?serverTimezone=UTC&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;rewriteBatchedStatements=true</property>
      	<property name="user">root</property>
      	<property name="password">123456</property>
        <property name="acquireIncrement">10</property>
        <property name="initialPoolSize">1</property>
        <property name="minPoolSize">1</property>
        <property name="maxPoolSize">100</property>
    
        <!-- intergalactoApp adopts a different approach to configuring statement caching -->
        <property name="maxStatements">0</property> 
        <property name="maxStatementsPerConnection">5</property>
      </named-config>
    </c3p0-config>
    
  2. dbcp

    driverClassName=com.mysql.cj.jdbc.Driver
    url=jdbc:mysql://localhost:33006/myemployees?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true
    username=root
    password=123456
    
    
  3. druid

    driverClassName=com.mysql.cj.jdbc.Driver
    url=jdbc:mysql://localhost:33006/myemployees?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true
    username=root
    password=123456
    

6.2 动态代理模式

代码示例:

package com.sxuek.pool;

import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.LinkedList;
import java.util.logging.Logger;

import javax.sql.DataSource;

import com.sxuek.util.DBUtils;

/**
 * 自定义数据库连接池
 * @author 11018
 *
 */
public class MyDataSource implements DataSource{
    
    
	//1、存储数据库连接使用的容器--连接池
	private LinkedList<Connection> pools =new LinkedList<>();

	public MyDataSource(int capacity) throws SQLException {
    
    
		if(capacity <=0 && capacity >=100) {
    
    
			System.out.println("容量不合法!");
		}else {
    
    
			for (int i = 0; i < capacity; i++) {
    
    
				//获取数据库连接--不能用,连接的close方法默认是释放资源的 而非将连接放回到数据库连接池
				//被代理对象  需要代理对象把被代理对象的close方法重写了
				Connection connection = DBUtils.getConnection();
				/**
				 * Java动态代理模式产生动态代理对象需要传递三个参数:
				 * 1、ClassLoader 类加载器
				 * 2、代理对象和被代理对象它两的共同实现的接口
				 * 3、InvocationHandler:调用方法时候,代理对象如何调用被代理对象的方法
				 */
				Connection proxyInstance = (Connection) Proxy.newProxyInstance(connection.getClass().getClassLoader(), connection.getClass().getInterfaces(), new InvocationHandler() {
    
    
					/**
					 * proxy 代理对象
					 * method 调用的代理对象的方法
					 * args 方法的参数
					 * 返回值 方法执行完成的返回值
					 */
					@Override
					public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
						/**
						 * 调用代理对象的方法如何最后执行被代理对象的方法
						 * 出来close
						 */
						String name = method.getName();
						if (name.equals("close")) {
    
    
							//连接放回数据库连接池
							pools.addLast((Connection)proxy);
						}else {
    
    
							Object invoke = method.invoke(connection, args);
							return invoke;
						}
						return null;
					}
				});
				pools.addLast(proxyInstance);
				
			}
		}
	}
	@Override
	public PrintWriter getLogWriter() throws SQLException {
    
    
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void setLogWriter(PrintWriter out) throws SQLException {
    
    
		// TODO Auto-generated method stub
		
	}

	@Override
	public void setLoginTimeout(int seconds) throws SQLException {
    
    
		// TODO Auto-generated method stub
		
	}

	@Override
	public int getLoginTimeout() throws SQLException {
    
    
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public Logger getParentLogger() throws SQLFeatureNotSupportedException {
    
    
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public <T> T unwrap(Class<T> iface) throws SQLException {
    
    
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public boolean isWrapperFor(Class<?> iface) throws SQLException {
    
    
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public Connection getConnection() throws SQLException {
    
    
		if(pools.size()>0) {
    
    
			Connection connection = pools.removeFirst();
			return connection;
		}else {
    
    
			System.out.println("数据库连接池无可用连接,请稍后重试");
			return null;
		}
	}

	@Override
	public Connection getConnection(String username, String password) throws SQLException {
    
    
		// TODO Auto-generated method stub
		return null;
	}

}

七、单元测试

【注意】:包下不能有与test重名的类

代码示例:

package com.sxuek.pool;

import org.junit.Before;
import org.junit.*;

/**
 * Java的单元测试工具Junit
 *   帮助我们进行代码测试的,通过Junit实现在一个类当中将普通的Java方法当作main函数来使用
 *  1、需要引入单元测试的依赖
 *  2、主要使用到了三个注解--都是只能声明在方法上
 *     @Before 一个Java类中可以存在一个  等同于Java中的代码块,会自动执行
 *        当@Test方法执行的时候会先执行@Before修饰的方法
 *        
 *     @Test    一个Java类中可以存在多个
 *        @Test修饰的方法可以当作普通的main函数来使用
 *     @After  一个Java类中可以存在一个
 *        当@Test方法执行结束会执行@After里面的方法
 *   
 * @author 11018
 *
 */
public class Demo01 {
    
    
	@Before
	public void before() {
    
    
		System.out.println("before的方法执行了");
	}
	
	@Test
	public void test01() {
    
    
		System.out.println("test01测试方法执行了");
	}
	@Test
	public void test02() {
    
    
		System.out.println("test02测试方法执行了");
	}
	@After
	public void destory() {
    
    
		System.out.println("destory方法执行了");
	}
}

猜你喜欢

转载自blog.csdn.net/cai_4/article/details/131464339
今日推荐