欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring-基于Spring使用自定义注解及Aspect实现数据库切换操作

程序员文章站 2022-06-17 21:55:38
实现思路重写spring的abstractroutingdatasource抽象类的determinecurrentlookupkey方法。我们来看下spring-abstractroutingdat...

实现思路

重写spring的abstractroutingdatasource抽象类的determinecurrentlookupkey方法。

我们来看下spring-abstractroutingdatasource的源码

Spring-基于Spring使用自定义注解及Aspect实现数据库切换操作

abstractroutingdatasource获取数据源之前会先调用determinecurrentlookupkey方法查找当前的lookupkey。

object lookupkey = determinecurrentlookupkey();
datasource datasource = this.resolveddatasources.get(lookupkey);
.......
return datasource;

lookupkey为数据源标识,因此通过重写这个查找数据源标识的方法就可以让spring切换到指定的数据源.

从变量定义中可以知道resolveddatasources为map类型的对象。

private map<object, datasource> resolveddatasources;

示例

Spring-基于Spring使用自定义注解及Aspect实现数据库切换操作

步骤一 新建maven工程

依赖如下: pom.xml

<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>com.artisan</groupid>
	<artifactid>dynamicdatasource</artifactid>
	<version>0.0.1-snapshot</version>
	<packaging>jar</packaging>

	<name>dynamicdatasource</name>
	<url>http://maven.apache.org</url>

	<properties>
		<project.build.sourceencoding>utf-8</project.build.sourceencoding>
		<file.encoding>utf-8</file.encoding>
		<spring.version>4.3.9.release</spring.version>
		<servlet.version>3.1.0</servlet.version>
		<aspectj.version>1.8.1</aspectj.version>
		<commons-dbcp.version>1.4</commons-dbcp.version>
		<jetty.version>8.1.8.v20121106</jetty.version>
		<log4j.version>1.2.17</log4j.version>
		<log4j2.version>2.8.2</log4j2.version>
		<testng.version>6.8.7</testng.version>
		<oracle.version>11.2.0.4.0</oracle.version>
		<jstl.version>1.2</jstl.version>
	</properties>

	<dependencies>
		<!-- spring 依赖 -->
		<dependency>
			<groupid>org.springframework</groupid>
			<artifactid>spring-beans</artifactid>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupid>org.springframework</groupid>
			<artifactid>spring-context</artifactid>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupid>org.springframework</groupid>
			<artifactid>spring-context-support</artifactid>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupid>org.springframework</groupid>
			<artifactid>spring-jdbc</artifactid>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupid>org.springframework</groupid>
			<artifactid>spring-webmvc</artifactid>
			<version>${spring.version}</version>
		</dependency>

		<dependency>
			<groupid>commons-dbcp</groupid>
			<artifactid>commons-dbcp</artifactid>
			<version>${commons-dbcp.version}</version>
		</dependency>


		<dependency>
			<groupid>org.aspectj</groupid>
			<artifactid>aspectjweaver</artifactid>
			<version>${aspectj.version}</version>
		</dependency>


		<dependency>
			<groupid>org.testng</groupid>
			<artifactid>testng</artifactid>
			<version>${testng.version}</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupid>org.springframework</groupid>
			<artifactid>spring-test</artifactid>
			<version>${spring.version}</version>
			<scope>test</scope>
		</dependency>

		<!-- oracle jdbc driver -->
		<dependency>
			<groupid>com.oracle</groupid>
			<artifactid>ojdbc6</artifactid>
			<version>${oracle.version}</version>
		</dependency>

		<dependency>
			<groupid>org.testng</groupid>
			<artifactid>testng</artifactid>
			<version>${testng.version}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupid>org.springframework</groupid>
			<artifactid>spring-test</artifactid>
			<version>${spring.version}</version>
			<scope>test</scope>
		</dependency>
		<!-- 
		<dependency>
			<groupid>log4j</groupid>
			<artifactid>log4j</artifactid>
			<version>${log4j.version}</version>
		</dependency>
	 	-->
		<dependency>
			<groupid>org.apache.logging.log4j</groupid>
			<artifactid>log4j-api</artifactid>
			<version>${log4j2.version}</version>
		</dependency>
		<dependency>
			<groupid>org.apache.logging.log4j</groupid>
			<artifactid>log4j-core</artifactid>
			<version>${log4j2.version}</version>
		</dependency>
		
	</dependencies>

	<build>
		<!-- 使用jdk1.7编译 -->
		<plugins>
			<plugin>
				<groupid>org.apache.maven.plugins</groupid>
				<artifactid>maven-compiler-plugin</artifactid>
				<version>3.1</version>
				<configuration>
					<source>1.7</source>
					<target>1.7</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

步骤二 继承abstractroutingdatasource并重写determinecurrentlookupkey方法获取特定数据源

package com.artisan.dynamicdb;

import org.springframework.jdbc.datasource.lookup.abstractroutingdatasource;

/**
 * 
 * 
 * @classname: dynamicdatasource
 * 
 * @description: 
 *  abstractroutingdatasource中的抽象方法determinecurrentlookupkey是实现数据源的route的核心
 *  .需要重写该方法
 * 
 * @author: mr.yang
 * 
 * @date: 2017年7月24日 下午8:28:46
 */
public class dynamicdatasource extends abstractroutingdatasource {

	@override
	protected object determinecurrentlookupkey() {
		return dynamicdatasourceholder.getdatasource();
	}
}

步骤三 创建dynamicdatasourceholder用于持有当前线程中使用的数据源标识

package com.artisan.dynamicdb;

/**
 * 
 * 
 * @classname: dynamicdatasourceholder
 * 
 * @description:创建dynamicdatasourceholder用于持有当前线程中使用的数据源标识
 * 
 * @author: mr.yang
 * 
 * @date: 2017年7月24日 下午8:23:50
 */
public class dynamicdatasourceholder {

 /**
 * 数据源标识保存在线程变量中,避免多线程操作数据源时互相干扰
 */
 private static final threadlocal<string> datasourceholder = new threadlocal<string>();

 /**
 * 
 * 
 * @title: setdatasource
 * 
 * @description: 设置数据源
 * 
 * @param datasource
 * 
 * @return: void
 */
 public static void setdatasource(string datasource) {
 datasourceholder.set(datasource);
 }

 /**
 * 
 * 
 * @title: getdatasource
 * 
 * @description: 获取数据源
 * 
 * @return
 * 
 * @return: string
 */
 public static string getdatasource() {
 return datasourceholder.get();
 }

 /**
 * 
 * 
 * @title: cleardatasource
 * 
 * @description: 清除数据源
 * 
 * 
 * @return: void
 */
 public static void cleardatasource() {
 datasourceholder.remove();
 }
}

步骤四 配置多个数据源和dynamicdatasource的bean

<?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:p="http://www.springframework.org/schema/p"
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:aop="http://www.springframework.org/schema/aop" 
 xmlns:tx="http://www.springframework.org/schema/tx"
 xmlns:util="http://www.springframework.org/schema/util"
 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
 http://www.springframework.org/schema/tx 
 http://www.springframework.org/schema/tx/spring-tx.xsd
 http://www.springframework.org/schema/aop
 http://www.springframework.org/schema/aop/spring-aop.xsd
 http://www.springframework.org/schema/util 
 http://www.springframework.org/schema/util/spring-util.xsd">
 
 <!-- 基类包,将标注spring注解的类自动转化bean,同时完成bean的注入 -->
 <context:component-scan base-package="com.artisan"/>
 
 <!-- 使用context命名空间,在xml文件中配置数据库的properties文件 -->
 <context:property-placeholder location="classpath:jdbc.properties" />
 
 <!-- 配置数据源--> 
 
 <!-- 主站点的数据源 -->
 <bean id="datasourcepr" class="org.apache.commons.dbcp.basicdatasource" 
 destroy-method="close"
 p:driverclassname="${jdbc.driverclassnamepr}"
 p:url="${jdbc.urlpr}"
 p:username="${jdbc.usernamepr}"
 p:password="${jdbc.passwordpr}" />
 
 <!-- 备用站点的数据源 -->
 <bean id="datasourcedr" class="org.apache.commons.dbcp.basicdatasource" 
 destroy-method="close"
 p:driverclassname="${jdbc.driverclassnamedr}"
 p:url="${jdbc.urldr}"
 p:username="${jdbc.usernamedr}"
 p:password="${jdbc.passworddr}" /> 
 
 <!-- 主站点cc实例数据源 -->
 <bean id="datasourcecc" class="org.apache.commons.dbcp.basicdatasource" 
 destroy-method="close"
 p:driverclassname="${jdbc.driverclassnamecc}"
 p:url="${jdbc.urlcc}"
 p:username="${jdbc.usernamecc}"
 p:password="${jdbc.passwordcc}" />


 <bean id="dynamicdatasource" class="com.artisan.dynamicdb.dynamicdatasource">
 <property name="targetdatasources" ref="dynamicdatasourcemap" />
 <!-- 默认数据源 -->
 <property name="defaulttargetdatasource" ref="datasourcepr" />
 </bean>
 
 <!-- 指定lookupkey和与之对应的数据源 -->
 <util:map id="dynamicdatasourcemap" key-type="java.lang.string">
 <entry key="datasourcepr" value-ref="datasourcepr" />
 <entry key="datasourcedr" value-ref="datasourcedr" />
 <entry key="datasourcecc" value-ref="datasourcecc" />
 </util:map>
 
 
 <!-- 配置jdbc模板 jdbctemplate使用动态数据源的配置 -->
 <bean id="jdbctemplate" class="org.springframework.jdbc.core.jdbctemplate"
 p:datasource-ref="dynamicdatasource" />
 
 <!-- 配置数据源注解的拦截规则,比如拦截service层或者dao层的所有方法,这里拦截了com.artisan下的全部方法 --> 
 <bean id="datasourceaspect" class="com.artisan.dynamicdb.datasourceaspect" />
 <aop:config>
  <aop:aspect ref="datasourceaspect">
  <!-- 拦截所有xxx方法 -->
  <aop:pointcut id="datasourcepointcut" expression="execution(* com.artisan..*(..))"/>
  <aop:before pointcut-ref="datasourcepointcut" method="intercept" />
  </aop:aspect>
 </aop:config>
 
 <!-- 配置事务管理器 -->
 <bean id="transactionmanager"
 class="org.springframework.jdbc.datasource.datasourcetransactionmanager"
 p:datasource-ref="dynamicdatasource" />
 
 <!-- 通过aop配置提供事务增强,让com.artisan包下所有bean的所有方法拥有事务 -->
 <aop:config proxy-target-class="true">
 <aop:pointcut id="servicemethod"
 expression="(execution(* com.artisan..*(..))) and (@annotation(org.springframework.transaction.annotation.transactional))" />
 <aop:advisor pointcut-ref="servicemethod" advice-ref="txadvice" />
 </aop:config>
 <tx:advice id="txadvice" transaction-manager="transactionmanager">
 <tx:attributes>
 <tx:method name="*" />
 </tx:attributes>
 </tx:advice>
</beans>

配置到这里,我们就可以使用多个数据源了,只需要在操作数据库之前只要dynamicdatasourceholder.setdatasource(“datasourcepr”)即可切换到数据源datasourcepr并对数据库datasourcepr进行操作了。

问题:每次使用都需要调用dynamicdatasourceholder#setdatasource,十分繁琐,并且难以维护。

我们可以通过spring的aop和注解, 直接通过注解的方式指定需要访问的数据源。 继续改进下吧

步骤五 定义名为@datasource的注解

package com.artisan.dynamicdb;
import java.lang.annotation.documented;
import java.lang.annotation.elementtype;
import java.lang.annotation.retention;
import java.lang.annotation.retentionpolicy;
import java.lang.annotation.target;

/**
 * 
 * 
 * @classname: datasource 
 * 
 * 
 * @description: 注解@datasource既可以加在方法上,也可以加在接口或者接口的实现类上,优先级别:方法>实现类>接口。
 *  如果接口、接口实现类以及方法上分别加了@datasource注解来指定数据源,则优先以方法上指定的为准。
 * 
 * @author: mr.yang
 * 
 * @date: 2017年7月24日 下午9:59:29
 */
@target({ elementtype.method, elementtype.type })
@retention(retentionpolicy.runtime)
@documented
public @interface datasource {
 // 和配置文件中 dynamicdatasourcemap中的key保持一致
 public static string pr_rb = "datasourcepr";

 public static string dr_rb = "datasourcedr";

 public static string pr_cc = "datasourcecc";

 /**
 * 
 * 
 * @title: name
 * 
 * @description: 如果仅标注@datasource 默认为pr_rb数据库实例
 * 
 * @return
 * 
 * @return: string
 */
 string name() default datasource.pr_rb;

}

步骤六 定义aop切面以便拦截所有带有注解@datasource的方法,取出注解的值作为数据源标识放到dynamicdatasourceholder的线程变量中

package com.artisan.dynamicdb;
import java.lang.reflect.method;
import org.aspectj.lang.joinpoint;
import org.aspectj.lang.reflect.methodsignature;

/**
 * 
 * 
 * @classname: datasourceaspect
 * 
 * @description: 
 *  定义aop切面以便拦截所有带有注解@datasource的方法,取出注解的值作为数据源标识放到dbcontextholder的线程变量中
 * 
 * @author: mr.yang
 * 
 * @date: 2017年7月25日 上午10:51:41
 */
public class datasourceaspect {

 /**
 * 
 * 
 * @title: intercept
 * 
 * @description: 拦截目标方法,获取由@datasource指定的数据源标识,设置到线程存储中以便切换数据源
 * 
 * @param point
 * @throws exception
 * 
 * @return: void
 */
 public void intercept(joinpoint point) throws exception {
 class<?> target = point.gettarget().getclass();
 methodsignature signature = (methodsignature) point.getsignature();
 // 默认使用目标类型的注解,如果没有则使用其实现接口的注解
 for (class<?> clazz : target.getinterfaces()) {
 resolvedatasource(clazz, signature.getmethod());
 }
 resolvedatasource(target, signature.getmethod());
 }

 /**
 * 
 * 
 * @title: resolvedatasource
 * 
 * @description: 提取目标对象方法注解和类型注解中的数据源标识
 * 
 * @param clazz
 * @param method
 * 
 * @return: void
 */
 private void resolvedatasource(class<?> clazz, method method) {
 try {
 class<?>[] types = method.getparametertypes();
 // 默认使用类型注解
 if (clazz.isannotationpresent(datasource.class)) {
 datasource source = clazz.getannotation(datasource.class);
 dynamicdatasourceholder.setdatasource(source.name());
 }
 // 方法注解可以覆盖类型注解
 method m = clazz.getmethod(method.getname(), types);
 if (m != null && m.isannotationpresent(datasource.class)) {
 datasource source = m.getannotation(datasource.class);
 dynamicdatasourceholder.setdatasource(source.name());
 }
 } catch (exception e) {
 system.out.println(clazz + ":" + e.getmessage());
 }
 }
}

步骤七 在spring配置文件中配置拦截规则

 <!-- 配置数据源注解的拦截规则,比如拦截service层或者dao层的所有方法,这里拦截了com.artisan下的全部方法 --> 
 <bean id="datasourceaspect" class="com.artisan.dynamicdb.datasourceaspect" />
 <aop:config>
  <aop:aspect ref="datasourceaspect">
  <!-- 拦截所有xxx方法 -->
  <aop:pointcut id="datasourcepointcut" expression="execution(* com.artisan..*(..))"/>
  <aop:before pointcut-ref="datasourcepointcut" method="intercept" />
  </aop:aspect>
 </aop:config>

步骤八 使用注解切换多数据源

extractdataservice.java
package com.artisan.extractservice;
import java.sql.resultset;
import java.sql.sqlexception;
import org.apache.logging.log4j.logmanager;
import org.apache.logging.log4j.logger;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.jdbc.core.jdbctemplate;
import org.springframework.jdbc.core.rowcallbackhandler;
import org.springframework.stereotype.service;

import com.artisan.dynamicdb.datasource;

/**
 * 
 * 
 * @classname: extractdataservice
 * 
 * @description: 业务类,这里暂时作为测试多数据源切换用
 * 
 * @author: mr.yang
 * 
 * @date: 2017年7月24日 下午9:07:38
 */

@service
public class extractdataservice {

 private static final logger logger = logmanager
 .getlogger(extractdataservice.class.getname());

 private jdbctemplate jdbctemplate;

 @autowired
 public void setjdbctemplate(jdbctemplate jdbctemplate) {
 this.jdbctemplate = jdbctemplate;
 }

 /**
 * 
 * 
 * @title: selectdatafrompr
 * 
 * @description:
 * 
 * 
 * @return: void
 */
 @datasource(name = datasource.pr_rb)
 public void selectdatafrompr_rb() {
 string sql = "select subs_id from owe_event_charge where event_inst_id = 10229001 ";

 jdbctemplate.query(sql, new rowcallbackhandler() {

 @override
 public void processrow(resultset rs) throws sqlexception {
 logger.info(rs.getint("subs_id"));
 }
 });
 }

 @datasource(name = datasource.dr_rb)
 public void selectdatafromdr_rb() {
 // 改为通过注解指定db
 // dynamicdatasourceholder.setdatasource(dbcontextholder.data_source_dr);
 string sql = " select a.task_comments from nm_task_type a where a.task_name = 'alarm_log_level' ";
 jdbctemplate.query(sql, new rowcallbackhandler() {

 @override
 public void processrow(resultset rs) throws sqlexception {
 logger.info(rs.getstring("task_comments"));
 }
 });
 }

 @datasource(name = datasource.pr_cc)
 public void selectdatafrompr_cc() {
 // dbcontextholder.setdatasource(dbcontextholder.data_source_cc);
 string sql = "select acc_nbr from acc_nbr where acc_nbr_id = 82233858 ";
 jdbctemplate.query(sql, new rowcallbackhandler() {

 @override
 public void processrow(resultset rs) throws sqlexception {
 logger.info(rs.getstring("acc_nbr"));
 }
 });

 }
}

步骤九 测试

package com.artisan;
import java.io.ioexception;
import org.apache.logging.log4j.logmanager;
import org.apache.logging.log4j.core.loggercontext;
import org.springframework.context.applicationcontext;
import org.springframework.context.support.classpathxmlapplicationcontext;
import org.springframework.core.io.resource;
import org.springframework.core.io.resourceloader;
import org.springframework.core.io.support.pathmatchingresourcepatternresolver;

import com.artisan.extractservice.extractdataservice;

/**
 * 
 * 
 * @classname: app
 * 
 * @description: 入口类
 * 
 * @author: mr.yang
 * 
 * @date: 2017年7月24日 下午8:50:25
 */
public class app {
 public static void main(string[] args) {
 try {
 // 加载日志框架 log4j2
 loggercontext context = (loggercontext) logmanager
 .getcontext(false);
 resourceloader loader = new pathmatchingresourcepatternresolver();
 resource resource = loader.getresource("classpath:log4j2.xml");

 context.setconfiglocation(resource.getfile().touri());

 // 加载spring配置信息
 applicationcontext ctx = new classpathxmlapplicationcontext(
 "classpath:spring-context.xml");
 // 从容器中获取bean
 extractdataservice service = ctx.getbean("extractdataservice",
 extractdataservice.class);
 // 从pr的rb实例中获取数据
 service.selectdatafrompr_rb();
 // 从dr的rb实例中获取数据
 service.selectdatafromdr_rb();
 // 从pr的cc实例中获取数据
 service.selectdatafrompr_cc();

 } catch (ioexception e) {
 e.printstacktrace();
 }

 }
}

其他代码

log4j2.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- log4j2使用说明:
使用方式如下:
private static final logger logger = logmanager.getlogger(实际类名.class.getname());
-->

<!--日志级别以及优先级排序: off > fatal > error > warn > info > debug > trace > all -->
<!--configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorinterval:log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration status="info" monitorinterval="180">

 <!-- 文件路径和文件名称,方便后面引用 -->
 <properties>
 <property name="backupfilepatch">d:/workspace/workspace-sts/backuporacle/log/</property>
 <property name="filename">backuporacle.log</property>
 </properties>
 <!--先定义所有的appender-->
 <appenders>
 <!--这个输出控制台的配置-->
 <console name="console" target="system_out">
  <!--控制台只输出level及以上级别的信息(onmatch),其他的直接拒绝(onmismatch)-->
  <thresholdfilter level="trace" onmatch="accept" onmismatch="deny" />
  <!-- 输出日志的格式-->
  <patternlayout pattern="%d{hh:mm:ss.sss} %-5level %class{36} %l %m - %msg%xex%n" />
 </console>
 
 <!--这个会打印出所有的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
 <rollingfile name="rollingfile" filename="${backupfilepatch}${filename}"
  filepattern="${backupfilepatch}$${date:yyyy-mm}/app-%d{yyyymmddhhmmsssss}.log.gz">
  <patternlayout
  pattern="%d{yyyy.mm.dd 'at' hh:mm:ss.sss z} %-5level %class{36} %l %m - %msg%xex%n" />
  <!-- 日志文件大小 -->
  <sizebasedtriggeringpolicy size="20mb" />
  <!-- 最多保留文件数 defaultrolloverstrategy属性如不设置,则默认为最多同一文件夹下7个文件,这里设置了20 -->
  <defaultrolloverstrategy max="20"/>
 </rollingfile>
 </appenders>
 
 <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
 <loggers>
 <!--过滤掉spring和mybatis的一些无用的debug信息-->
 <logger name="org.springframework" level="info"></logger>
 <logger name="org.mybatis" level="info"></logger>
 <root level="trace"> 
  <appender-ref ref="rollingfile"/> 
  <appender-ref ref="console"/> 
  </root> 
 </loggers>
</configuration>

jdbc.properties

##########################
##
##
## dbcp datasource pool ,basic configuration first.
## the other parameters keep default for now , you can change them if you want 
##
##
##########################

#database in lapaz
jdbc.driverclassnamepr=oracle.jdbc.driver.oracledriver
jdbc.urlpr=jdbc:oracle:thin:@172.25.243.4:1521:xx
jdbc.usernamepr=xxx
jdbc.passwordpr=xxxxxxxx

#database in scluz
jdbc.driverclassnamedr=oracle.jdbc.driver.oracledriver
jdbc.urldr=jdbc:oracle:thin:@172.25.246.1:1521:xx
jdbc.usernamedr=xxx
jdbc.passworddr=xxxxxxx

#database in lapaz
jdbc.driverclassnamecc=oracle.jdbc.driver.oracledriver
jdbc.urlcc=jdbc:oracle:thin:@172.25.243.3:1521:xx
jdbc.usernamecc=xxx
jdbc.passwordcc=xxxxxx

运行结果:

Spring-基于Spring使用自定义注解及Aspect实现数据库切换操作

代码

https://github.com/yangshangwei/dynamicdatasource

以上这篇spring-基于spring使用自定义注解及aspect实现数据库切换操作就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。

上一篇: MVC三层架构

下一篇: SQL distinct用法