当前位置:   article > 正文

WebServer代码实现第四版_swl server后端代码

swl server后端代码

前言

1.JDBC批处理与返回自动主键

  • 关于JDBC,已经介绍了基本的增删改查,事务,分页等内容,那么还有两知识点,一个是批量更新,一个是返回自动主键。最后,我们对JDBC涉及的相应的业务再进一步的封装,就是DAO(Data Access Object),来进一步重构WebServer的业务功能。
  • 那事务呢,我们已经知道了,事务实际上是在这个Connection当中去控制的,而且我们知道,jdbc默认是什么啊,自动提交事务的,是吧,那我们要想这个,自己去这个控制事务,那怎么办啊,我们需要,首先我们要把自动提交事务的那个给它关了,是吧,然后,我们可以自己怎么样呢,commit或者rollback,是吧,那么事务,我们将来在什么地方去控制呢,是在你的业务当中去控制,比如说,你准备做一个业务,像我们之前写的Test.java,做一个转帐业务,那实际上,转账我们说还要做一些必要的验证工作呢,比如你得看看,那个转出帐号的中的钱够不够啊,是吧,等等那些事啊。
  • 所以就是说,我们在这块操作的这个过程中,实际上,我们这个事务的开启,是不是可以在你整个业务的开始,是吧,然后你再进行一系列的操作,包括你更新的那两条记录,最后,你发现都干完了,提交事务啊,最后跳转页面,将来你们在使用Tomcat也是一样,我们最后跳页面,都是先那个,该走的流程都走完,然后这个提交事务,提交好跳页面,给用户看结果,就可以了啊。

JDBC批处理:JDBCDEMO_batch.java

  • 那么下面,我们来看一看这个批量更新,其实批量更新呢,就是相当于我们之前学的,那个插1000条数据,那我们知道,插1000条数据的话,影响这个数据库里面执行的效率的有3点,第一个是什么啊,就是我们之前使用的PreparedStatement,为什么要用它呢,是因为可以重用执行计划,这个没有必要为每一条记录都生成一次,就是每一次操作都生成一个执行计划,是吧,如果你都是往一张表里插数据,语义是没有变化的话,这样就使用什么啊,同一个执行计划就好了,就每次换值就行了。
  • 那么,减少这个执行计划的生成,这是提高效率的第一点,还有一点就是什么啊,事务,我们说你插一条提交一次事务,插一条提交一次事务,那这样的话,效率就低,是吧,那我们可以干嘛呢,一次性这个去提交事务啊,这个呢,我们之前会发现,那个效果还挺显著的,是吧,还有一点呢,就是批量更新,批量更新它解决的是什么问题呢,传输问题,网络传输时,就是在客户端与服务端,我们用那个PreparedStatement,我们也在一个事务里干,但是我们是不是每一次,把里面那个问号替换好了,调一次executeUpdate执行,但你知道,你每次调executUpdate,都要有一些网络请求的,就是你把你这3个问号,或者几个问号值发给数据库,你执行一次,是吧,我再发给你,你再执行,再发给你,再执行,等于说,我是不是向数据库发送了1000次请求,是吧,把这1000个sql语句发过去,那么其实我们可以在这个地方呢,也可以怎么样呢,是不是可以批操作。
  • 怎么来批操作呢,我们先在我们本地缓存好,把你要干的事都缓存好,比如我1000条都存好了,一口气给你,我给你1000条,是吧,一次性给你,我在拿到以后,是不是这1000条一次性处理,明白吧,那这样的话,也可以提高效率啊,那我们现在就来看看,批量更新是怎么实现的啊,批量更新,其实在Statement也好,PreparedStatement也好啊,都可以这个进行使用啊,那么它就有这么一个方法叫作,addBatch(String sql),原来咱们想执行的话,你调execute,什么Update啊,executeQuery,那你是不是就是直接就执行了那条sql语句,是吧。
  • 现在也可以先把它怎么样呢,放到批操作里,addBatch(String sql),这是Statement原生的就支持的方法,就是增加一个批操作啊,把你的sql语句先搁到,addBatch(String sql)里,就是先不发给数据库啊,那你这想执行一个,就先放到addBatch(String sql)这,想执行一个,就放到这啊,当你都放好了以后,你这块干嘛呢,executeBatch(),就是你一次性执行这一批,那就相当于把那个,刚才那个存的那些sql语句,一口气全发给数据库,让它去执行,这样的话,可以减少网络调用啊,因为服务器和数据库可能不是在一个地方,它们之间的数据传输是需要网络开销的。然后呢,当你这些事都干完了以后,最后可以怎么样呢,clearBatch()一下,把你本地这些缓存清了啊,就完事了。
  • 那么现在在demo包里面建一个类,叫JDBCDEMO_batch,将try-catch-finally,建立连接等基础工作做完以后,我们在try里面,比如说啊,咱们在这往表里面插1000条记录,那我应该怎么干,实际上,我们使用for循环,如果我们使用普通的Statement的话,这么写啊,Statement state = conn.createStatement();,但实际上,如果你要里面有动态数据的话,其实就不应该用它,这样的话,你执行计划会生成很多次,是吧,这个不理想啊,但是咱们先用着,用Statement的目的是告诉大家,其实它也有批操作啊,`for(int i=0; i<100; i++) {}``,少来点吧,就100条记录,这个当然就无所谓了啊,那这样的话,就相当于我执行很多次,是吧。
  • 执行的话,怎么做呢,那么首先,是不是先写个sql语句,这里写insert语句,是这样的吧,但批操作不是说只能执行INSERT,这个UPDATE,DELETE什么的,都可以执行啊,包括什么,这个查询都可以用啊,只不过一般查询的话,就一次查询,就一次结果了,所以那个,我们一般不会使用批操作,所以一般我们说批操作,还是就那个DML操作多一些啊,那么比如说,我们这块执行一下,

String sql = "INSERT INTO userinfo_Van "
	         + "(id,username, password, nickname,account) "
	         + "VALUES "
	         + "(seq_userinfo_Van_id.NEXTVAL,'test','123123','test',5000)";
  • 1
  • 2
  • 3
  • 4

  • 首先,INSERT INTO userinfo,是吧,然后比如说加"(id,username, password, nickname,account) ",然后呢,"VALUES ",给值,"(seq_userinfo_Van_id.NEXTVAL,'test','123123','test',5000)";,这个地方如果要是有这个动态的信息,是不是就不太适合使用Statement来执行了,是这样的吧,那像我这,如果没有任何动态信息的话就无所谓了,是这样的吧,那当然,这个咱们给它加一个东西,加一个吧,用户名改成"test"+i,这样的话,就其实不太适合使用它了啊,但是咱们用它说问题,"(seq_userinfo_Van_id.NEXTVAL,'test"+i+"','123123','test',5000)";,那比如说,我这个sql语句写好了,那么下一步,咱们原来是不是就执行了,那怎么执行啊,直接调Statement点executeUpdate(sql),是这样的吧,执行一条sql语句,state.executeUpdate(sql);,那这样的话呢,我们就执行了。
  • 那注意,如果这样执行的话,你每执行一次,它就会把那条sql语句发给数据库一次,是吧,而你的数据库还得给它生成一个执行计划,对么,那么在这个地方,我们为了减少网络调用啊,我怎么做呢,这样干啊,我这个,state.executeUpdate(sql),这条语句先不执行,我干嘛呢,我调用这个,Statement的一个方法叫作,addBatch(sql);,也就是说,先把这条sql语句缓存,我先这个缓存到这个批操作中啊,那么这样的话,缓存好了以后,大家注意,state.addBatch(sql);,你这句话,发给数据库了么,并没有,是在我们自己内存当中的,清楚吧,那比如说,当我这1000条,都干完了以后,我干嘛呢,一口气发送,它怎么一口气发送呢,你们看啊,我这要是一出了这个循环,是不是相当于这1000条信息是不是都缓存进去了,对吧,

for(int i=0; i<100; i++) {
	String sql = "INSERT INTO userinfo_Van "
		         + "(id,username, password, nickname,account) "
		         + "VALUES "
				 + "(seq_userinfo_Van_id.NEXTVAL,'test"+i+"','123123','test',5000)";	
//每次调用executeUpdate()都会发送给数据库
//	state.executeUpdate(sql);
	state.addBatch(sql);//先缓存到批中
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  • 然后,出来以后呢,我在这块执行另一个方法,就是叫做,state.executeBatch();,那它就一次性,就把你刚才缓存的东西,全发给数据库,一口气就执行啊,而且大家注意,它的返回值是这样的,它返回给我们一个什么呢,int数组,int[],那返回的这个int数组,是什么意思呢,就是说,影响表中多少条数据,是吧,当然这回是个数组,原来咱们执行state.executeUpdate(sql);方法,不是返回一个int值么,是不是影响表中多少条数据,因为你这是一次性给了1000条啊,像我这给了100条,是吧,你给了100条,是不是每一条sql语句,是不是都可以影响表中数据的,对么,那这个时候它返回的就是一个数组,而数组里面每一个元素,就是你曾经执行的其中的一个(语句),影响的表中的数据。
  • 也就是说,我不是执行了100条么,INSERT语句么,那它是不是每一条INSERT语句都会有一个结果了,对吧,那它就会放到这个int数组当中,明白吧,数组的每一个元素,就是你执行的其中的之一,影响了表中的多少条数据,能理解吧,但大家注意啊,它这个state.executeBatch()里面的这个返回值啊,有的时候,说我执行成功了,它返回值为什么不是1啊,它在那给我写个什么-1,什么-2,或者或几啊几,这个都是有可能的,这个东西,说返回-1或-2,并不是说,指的是执行失败了啊,这在jdbc里面呢,是有说明这个问题的啊,它是怎么说的呢,它是这样的啊,打开文档看看,比如说,找Statement的executeBatch,就这个方法,对吧,

int[] executeBatch() throws SQLException
Submits a batch of commands to the database for execution and if all commands execute successfully, returns an array of update counts. The int elements of the array that is returned are ordered to correspond to the commands in the batch, which are ordered according to the order in which they were added to the batch. The elements in the array returned by the method executeBatch may be one of the following:
A number greater than or equal to zero -- indicates that the command was processed successfully and is an update count giving the number of rows in the database that were affected by the command's execution
A value of SUCCESS_NO_INFO -- indicates that the command was processed successfully but that the number of rows affected is unknown
If one of the commands in a batch update fails to execute properly, this method throws a BatchUpdateException, and a JDBC driver may or may not continue to process the remaining commands in the batch. However, the driver's behavior must be consistent with a particular DBMS, either always continuing to process commands or never continuing to process commands. If the driver continues processing after a failure, the array returned by the method BatchUpdateException.getUpdateCounts will contain as many elements as there are commands in the batch, and at least one of the elements will be the following:

A value of EXECUTE_FAILED -- indicates that the command failed to execute successfully and occurs only if a driver continues to process commands after a command fails
The possible implementations and return values have been modified in the Java 2 SDK, Standard Edition, version 1.3 to accommodate the option of continuing to process commands in a batch update after a BatchUpdateException object has been thrown.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

  • 它这块告诉你了,返回的数组元素可能是以下这种,一个大于等于0的数,这就等于你执行成功了,然后,影响了表中多少条记录,这是正常的,是吧,然后,它有可能还会给你返回一些常量值,比如说,这个SUCCESS_NO_INFO,什么意思啊,就是这条语句执行成功了,但是没有任何结果啊,就是数据库没有给我们任何回应,原因是什么呢,它说受影响的行数是未知的啊,那这句话怎么理解,其实它太草率了,这句话说的,告诉大家一点,是因为不同的jdbc厂商,就是我们那个数据库厂商,它在实现jdbc接口的时候,它返回,它就按照它自己的意愿返回了,那可不一定大家都一样,只是说我们说接口的方法都一样,是吧,但是返回,它这个概念,有可能不是按照你说。
  • 比如说,我正常情况下,咱执行了100条INSERT语句,每条是不是返回结果,都应该是1,因为都往表里,插一条记录么,是吧,但你发现有的数据库,它就不给你返回1,数组里面那100个,它都不是1,是吧,它就给你返回,你这个什么呀,常量值,比如它这还有什么执行失败的啊,这还有什么执行成功的啊,等等,这种情况它都会有啊,那么,这个东西是在哪呢,SUCCESS_NO_INFO,注意这东西,我们有什么呢,有常量去对应的啊,都在这啊,

/**
 * The constant indicating that the current <code>ResultSet</code> object
 * should be closed when calling <code>getMoreResults</code>.
 *
 * @since 1.4
 */
int CLOSE_CURRENT_RESULT = 1;

/**
 * The constant indicating that the current <code>ResultSet</code> object
 * should not be closed when calling <code>getMoreResults</code>.
 *
 * @since 1.4
 */
int KEEP_CURRENT_RESULT = 2;

/**
 * The constant indicating that all <code>ResultSet</code> objects that
 * have previously been kept open should be closed when calling
 * <code>getMoreResults</code>.
 *
 * @since 1.4
 */
int CLOSE_ALL_RESULTS = 3;

/**
 * The constant indicating that a batch statement executed successfully
 * but that no count of the number of rows it affected is available.
 *
 * @since 1.4
 */
int SUCCESS_NO_INFO = -2;

/**
 * The constant indicating that an error occurred while executing a
 * batch statement.
 *
 * @since 1.4
 */
int EXECUTE_FAILED = -3;

/**
 * The constant indicating that generated keys should be made
 * available for retrieval.
 *
 * @since 1.4
 */
int RETURN_GENERATED_KEYS = 1;

/**
 * The constant indicating that generated keys should not be made
 * available for retrieval.
 *
 * @since 1.4
 */
int NO_GENERATED_KEYS = 2;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

  • 看到了吧,这个SUCCESS_NO_INFO,然后它的常量值,你看它得-2,明白吧,有可能一会你发现,它那数组里面,一溜的-2,那-2,不是说表示我一条没插进去,还欠它两条,不是这意思啊,就是说,它还是成功了的,明白吧,就是这东西,你要不看文档,你就就就呈费解了,是吧,所以我们说,还是要自己去看一看文档啊,那总之,我们说你这样的话,一执行的话,就一口气,全发过去了,明白吧,那么,执行完了以后呢,我们这块正常情况下,你就去输出执行完毕,但大家注意,这样的话,效率很低的,为什么说效率低,咱们这块仅仅是把什么啊,网络传输这个地方,是不是给提高速度了,对吧,但是有两个没有解决,一个就是执行计划生成没管,还有一个那个,就是事务没管,是吧,你要想飞快,那你得3条都考虑,对吧。
  • 但至少我们可以看到什么呢,Statement,本身就支持这个批处理了,是吧,那么这是Statement的执行,但是这个东西我们不这样干了,为什么呢,因为这个,你只要里面有动态信息,不适合用Statement干,是吧,但是Statement就相当于是,你每次拼好一条sql语句的时候,就不要调executeUpdate了,而是直接调addBatch,把这sql语句先存上,然后一口气全发过去,明白么,它是这种操作,那么我们说,如果要是执行动态信息的话,我们应该用什么啊,用PreparedStatement,而PreparedStatement是不是上来,我们先把sql语句写好,是吧,所以咱们上来呢,写好sql啊,那这个sql应该怎么写啊,

String sql = "INSERT INTO userinfo_Van "
		     + "(id, username, password, nickname, account) "
		     + "VALUES "
		     + "(seq_userinfo_Van_id.NEXTVAL,?,'123123','test',5000)";
  • 1
  • 2
  • 3
  • 4

  • 首先,INSERT INTO userinfo,然后呢,(id,username,password,nickname,account),然后呢,VALUES,然后呢,(seq_userinfo_id.NEXTVAL,?,?,?,5000),然后,问号,问号,问号,这就别问号了,这个这个,密码也别问号了,统一的,123123,我就写一个问号了,反正明白意思就行了啊,行吧,我就写一个问号了,这明白怎么回事就行了,就相当于这个问号,是一个变化的值,(seq_userinfo_id.NEXTVAL,?,'123123','test',5000),是吧,然后,那我想执行1000次的话,首先,第一点,咱们要先创建PreparedStatement,是吧,PreparedStatement ps = conn.prepareStatement(sql);,然后就把sql语句给进去,是吧。
  • 然后呢,我们来for(){}循环,然后呢,咱们在里面就替换问号了,是吧,咱们原来怎么做的,替换问号,是不是就是ps.set,比如说String,是吧,第一个问号的值,它比如说是那个,"test"+i,然后呢,我们是不是要正常执行,就调executeUpdate,是这样的吧,当你调完这executeUpdate,就等同于什么啊,它不就把这问号里的值发给数据库了,你执行一下,刚才生成好了那个执行计划,是这意思吧,你使用PreparedStatement的好处在于什么呀,执行计划是不是已经提前生成好了啊,你只需要每次传问号对应的值就完了,那你这样的话,你每一次执行,是不是都会把这问号的值,传一遍过去的,是这样的吧。
  • 那么现在,我们也不这样干,我们怎么干呢,ps.addBatch(),而且这块的addBatch,是不是就不用传sql语句了,为什么上面的那个要传啊,就是因为Statement,你每次执行的时候,是不是都要把这sql语句发过去的,对么,而PreparedStatement,已经上来,已经把sql语句发过去了,我只是不知道问号对应的值了么,所以实际上你只是,把每次你设置的那个问号,对应的值干嘛呢,缓存起来了,也就是说,你这ps.addBatch(),缓存了一次,第一次缓存,那个值就知道啊,这个问号代表的值就是这个"test"+i,你下次再来的时候,是不是就是另一组了,明白吧,等于说你把每一组问号换的值,都在这里存好,然后,最后干嘛呢,也是ps.executeBatch();,能理解吧,那同样你可以得到这个int型的数组啊,然后呢,是不是就执行完毕了,对吧。
  • 那这样的话,一样,我就把我网络调用这个事搞定了,是吧,那你要想最快,还应该干嘛,应该再把事务开开,因为PreparedStatement,实际上,执行计划是不是已经重用了,对吧,你要想最快的话,你就在sql语句之前做一件事情,就是conn.setAutoCommit(false);,然后在这个地方,都完事了以后,在这块,conn.commit();,大家注意,那这样的话,执行速度是最快的,一个事务里,是吧,一个执行计划,一次网络传输,全搞定了啊,那也就是说,将来等你们真的要,批量操作的时候,就应该这么干。
  • 然后,int[] data = state.executeBatch();,这个是使用传统的Statement执行executeBatch,PS执行的批处理是int [] data = ps.executeBatch();,一样的方法,另外,这个地方还会有一个问题啊,将来比如说,我们批操作的话,说这个也不能无止境的往ps.addBatch();里面发啊,比如说,我准备发送一万条数据,是吧,全搁到批里,一口气过去一万个,你也得看你的内存承载能力啊,有的时候,你这搁着,搁着,内存溢出了啊,这种情况也存在,所以你可以干嘛呢,比如说在for循环里面是不是也可以记一下,比如说,我每500条提一次,每500条提一次,可不可以,我们想每500条提一次怎么做啊,我先是不是可以。
  • 比如我在这加一个什么呢,int count = 0;,然后呢,你在里面加了一次呢,就什么count++;一次,是这意思吧,然后,如果比如说,if(count%500==0) {},什么意思,就是它是500的倍数了,500的倍数,就说明它够500条了,然后就可以在这个if里面干嘛呢,执行一次,ps.executeBatch();,就是一次过去500条,清楚吧,过去完了以后呢,ps.clearBatch();,把你之前这个缓存那些东西,先从你本地清了,明白吧,清掉以后,你再执行,再执行,那你就,这样就完了,能理解么,你这么干就等于说,500条过去一次,500条过去一次,你也别就一口,当然我们这本身才,总共才循环100次,但我是那个意思,明白么,像在我这块呢,比如说我每50次过去一次,清楚吧,你这样的话,你就能保证,你这个也是分段。
  • 咱们比如说,这个50次,你就要一次网络调用,那有时候,你太多也不行啊,当然,这个东西,说这个量怎么去取,那这还得结合你将来的硬件实例啊,有很多因素啊,当然大家可以自己去开发的时候,你们也可以在,有这需要的一个测试么,是吧,看我大概到多少条的承载程度,就差不多了,你就在这之前,定一个量,是吧,然后,大概这么多条就过去一次,过去一次啊。
  • 然后,如果最后一次不是50的倍数,在循环for循环之外,是不是还要再额外executeBatch()一次啊,int [] data = ps.executeBatch();,也可以比如说,是不是executeBatch()可以得到这个数组了么,是吧,你可以放入循环,把这个数组输出一下,看看哪一个值都是什么,可以先输出一下数组的length啊,然后再输出一下这个数组里的每个元素,看看执行完了以后是什么,你有可能看到的,就是一堆的-2,是吧,-2什么意思来着,success_no_info,是吧,就是它执行成功了,但是没有消息啊,这种情况也存在啊,这个是由于这个不同的这个数据厂商的,提供的jdbc驱动的时候,它的这个实现可能不一样啊,所以它里面,有可能返回的不是具体影响了多少条记录了啊。
代码实现:
package demo;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Statement;

/**
 * 批操作
 * 批操作是可以将要执行的大量SQL语句缓存在本地
 * 然后一次性发送给数据库,减少网络调用,调高
 * 执行效率。
 * @author soft01
 *
 */
public class JDBCDEMO_batch {
	public static void main(String[] args) {
		Connection conn = null;
		try {
			conn = DBUtil.getConnection();
			//Statement state = conn.createStatement();
			/*for(int i=0; i<100; i++) {
				String sql = "INSERT INTO userinfo_Van "
					         + "(id,username, password, nickname,account) "
					         + "VALUES "
					         + "(seq_userinfo_Van_id.NEXTVAL,'test','123123','test',5000)";
			//					+ "(seq_userinfo_Van_id.NEXTVAL,'test"+i+"','123123','test',5000)";	
			//每次调用executeUpdate()都会发送给数据库
			//	state.executeUpdate(sql);
				state.addBatch(sql);//先缓存到批中
			}
			int[] data = state.executeBatch();*/
			
			conn.setAutoCommit(false);//把事务打开
			String sql = "INSERT INTO userinfo "
					     + "(id, username, password, nickname, account) "
					     + "VALUES "
					     + "(seq_userinfo_id.NEXTVAL,?,'123123','test',5000)";
			PreparedStatement ps = conn.prepareStatement(sql);
			int count = 0;
			for(int i=0; i<100; i++) {
				ps.setString(1, "test"+i);
//				ps.executeUpdate();//原来的方案
				ps.addBatch();
				count++;
//				if(count%500==0) {
				if(count%50==0) {
					ps.executeBatch();
					ps.clearBatch();
				}
			}
			int [] data = ps.executeBatch();
			int d=data.length;
			System.out.println("data.length:"+d);
			for(int i=0;i<d;i++) {
				System.out.print(" "+data[i]);
			}
			
			conn.commit();
			System.out.println("执行完毕!");
					
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			DBUtil.closeConnection(conn);
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

题外话
  • 那想要执行速度是最快的,就要一个事务里,是吧,一个执行计划,一次网络传输,全搞定了啊,将来真的要批量操作的时候,就应该这么干。我告诉你,速度差的非常多,我们曾经做过一个事情,就是当时,我们在做这个游戏的时候,我在写一个log日志的服务器啊,这个啊,大家知道实际上将来这个,你们做东西的时候,经常会有分布式么,分布式是什么意思啊,3个臭皮匠顶一个诸葛亮啊,什么意思,就是说,将来经常会要听到一个问题就是高并发,现在我们知道什么叫并发啊,就是同时要干多件事,就相当于线程,不就是并发操作么。
  • 我们说,在计算机里面没有真正意义上的同时干叫并发,是吧,那高并发,意思就是相当于,我要大量的同时干,它那个同时干,对计算机来讲,都不是真的同时干,但你就意义上理解为就相当于,举一个简单的例子吧,玩游戏也一样,我一个网游,你们大家是不是同时都连,往我这个线程(池)里面要,是吧,大家都连上,那这样的话,是不是服务器就高并发了,一台机器,我告诉你,搞到死它也处理不了这么多了,它怎么玩啊,将来出去工作的时候,也有可能会碰到这种问题啊,当我要处理高并发的时候,有什么解决方案没有,是吧,我们一般情况下,是要做业务拆分的啊,就相当于要分布式。
  • 怎么干的,就相当于啊,一台服务器,比如说我们要处理业务逻辑复杂一点的话,两三百个并发,很了不起了已经,业务复杂一点的啊,那说简单的业务可能还好啊,比如说,业务稍微复杂一点的啊,两三百个并发已经很了不起了,什么意思啊,就是同一时间,你这服务器能承载两三百个人,很牛了,你这服务器应该很牛了,说那我两三百个根本不够啊,是吧,那你看我现在玩什么游戏,就两三百个人登,像我们现在是不是都好几万人在线,同时好几十万人在线,对吧,那你看,服务器那边怎么,怎么布置,它首先分服,是吧,电信一服,电信二服,电信三服,是有这样的吧,先把里面那个给你拆散,一波一波的人,是吧。
  • 我以魔兽为例吧,魔兽都玩过么,魔兽世界啊,以前没有玩过的,是不是,那还是王者荣耀吧,是吧,反正就这意思吧,就是我没玩过这个,这个,因为我现在已经,已经退出了,这个这个,我已经看破,看破了啊,看破了,是吧,就举这例子吧,是吧,就比如说,你是不是先分服,你去哪个服务器玩,这样的话,是不是就把人首先分开了在每台服务器上,我们客户端在连上的时候,比如说,你这个手机,他的手机是吧,比如说,咱俩连的是同一个服,上来的时候,大家注意啊,我们会有一台分压的服务器,它实际上,不处理任何业务逻辑,它不处理任何业务逻辑,它就是相当于帮你做转发的,你要干什么事,我给你转发给别的服务器干。
  • 也就是说,我们后面还要怎么样呢,一组的服务器啊,它都是一组服务器,它怎么样呢,处理你这游戏里面的各个业务,比如说什么呢,没错,它负责走路的,它负责使某些技能的,可能你那技能还会拆分,法系技能的,什么啊,这个物理技能,是吧,等等各种技能,它要给你做拆分,然后上面,这一块,还有一台统一的什么呢,数据共享的服务器,比如说你人物的血啊,什么都在这块拿到的,也就是说,谁想要这些数据,都是从你这台服务器上面干嘛呢,去取啊,然后这些服务器帮你去做什么呢,各种业务,明白吧,打小怪的,是吧,控制小怪血量的,什么这那,这那,一大堆啊。
  • 那也就是说真正要打一个怪的话,比如说,我说我要揍谁,你要把它kill,你要揍哪个,使什么技能啊,那就让它处理,等于那个让它处理,那这样的话,大家注意,多台服务器一起处理的话,是不是大家的访问量是可以保证的,明白么,就是那,包括那会我玩魔兽的时候,特别什么,我老会给我们这个工会里面人,去普及这些知识啊,告诉他们,我说这个怎么回事,是吧,原来它们老说,我靠,说这个,我进不了副本了,是吧,这一进副本,进不去,他说,哎,我这个,那个,什么那个拍卖行点不开了,我们怎么跟NPC交不了任务,知道吧,有好多好多这样的问题,给他们解释。
  • 我说什么啊,我说这个啊,不是一台机器干,我说这个是一个集群啊,我说有专门处理拍卖行的,有处理NPC对话的,是吧,有打怪,有使技能的,有副本服务器,是吧,我说,甚至地图都是不同服务器,是吧,阿格恶玛就算是单独一条,单独服务器,是吧,暴风城市一个啊,然后告诉大家什么,这个,***这么回事,什么这那,这那,告诉他,实际上我们说是,一组服务器来架这一个游戏的啊,所以为什么,有时候你会发现,你能打怪啊,能聊天,就是拍卖行点不了,为什么呢,拍卖行服务器挂了,就这台服务器宕机了,明白吧,那就这个功能,你使不了,明白么,那就等着什么啊,跟GM说一下,让他重启一下,那个机房把这台机器给我重启一下就好了,明白吧,他们都是这么干啊,哎,我怎么说到这了啊,log,是吧,说到那个log服务器啊。
  • 那我告诉你们log日志这台服务器,是非常不一样的一台机器啊,它是相当于什么呢,整个咱们这个服务器里面的这个啊,记账的,在这里我就相当于记账的,记事的管事员,就是它就是属于,你们谁干什么事,我都能给你记下来,你干过什么,你干过什么事啊,就是所有人都要向它报告,它在这做汇总,去记录啊,为什么要记录呢,是因为啊,很多很多情况吧,举个简单例子,外挂啊,外挂,大家注意,日志,我们可以通过分析日志,就能看得出来,为什么呢,我告诉你们啊,服务器实际上很傻,它根本就不处理外挂,也就是,它不知道你使不使外挂,明白么,或者说它很少会去管,但是我们也不是说一点机制都没有,处理外挂的机制有很多啊。
  • 我告诉你们啊,这个,你们现在知道外挂是什么么,知道么,不是啊,外挂都知道是什么,是吧,java不适合写外挂啊,先告诉你们,想学外挂,写C++吧,这个C++是比较适合写外挂,还有,当然还有一些,这个主流的一些语言,也可以写外挂啊,我先给大家说说,外挂的原理啊,这是一个客户端对一个服务端,咱们现在其实已经知道,客户端跟服务端之间的通讯了,是吧,你只是互相交流么,你发数据,那就你自己去定义格式,只不过前面是咱的游戏,其实之前做的飞机大战,也可以做成网游版,俩人一起玩么,什么俩,怎么俩,有俩小飞机啊,你一个,他一个,是吧,服务器那面建立,给你们俩,你们俩都建立好连接,你把你的飞机位置,告诉给服务器,发给他,他把他飞机的位置,也告诉你,你们俩不就可以互相在屏幕上画出两驾飞机么,就一直在发送你的位置,发送给服务器,他也发送,互相传一下么,就跟聊天一样么,互相转发,是吧。
  • 然后,客户端拿到以后,知道他的飞机位置在哪,画在那不就完了么,不就这意思么,你一动,你就一回过去,你就动,就发发发发,他这,服务器告诉他这,它也就跟你这跟着动了,能理解这意思吧,就这个意思啊,那么所以就是,玩游戏其实本质就是,我客户端一直在跟服务器通讯,告诉你,我的状况,服务器再告诉我周边世界的状况,是这样的吧,然后呢,比如说,有一个怪离我特别远,我想坎它,我发现这是一个系统精英,它能掉好东西,但我打不过它,是吧,比如我现在装备不行,我纯粹被人辗压的,是吧,这个你意识再犀利,手法再风骚,没有用,是吧,过去就是一刀秒,是吧,都是一刀秒,那你这时候,你怎么办。
  • 那我说,我就离远点打,离远点打,你一使技能,它告诉你太远了,够不着,必须得离的近才能够得着啊,对吧,这是为什么,这是客户端判定了,客户端它判定,你和它的距离过远,我就不会再跟服务器说,咱们揍它,只有走到它跟前,我发现咱俩距离够了,你说你揍它,客户端就跟服务器说,我揍它,服务器说行,看你的攻击度,强度啊,这个那这个那,还看它防御力啊,怎么的,经过一个公式一算,它掉多少血,服务器给它掉好了,它告诉你它掉多少血,然后,你看那边,腾,然后给你掉一个血,明白么,就是这么个原理啊。
  • 但是你要是离的远,你不就够不着,你不就不发送么,外挂是干嘛的呢,外挂,就相当于是在客户端的外边,包一个壳,明白吧,包一个壳,你不是不跟客户端说,你揍它么,我跟它说,因为你得知道,发送网络请求,就是发送数据么,你不发,我给你发,我帮你发,是吧,也就相当于什么呢,它给你发一句,我揍它,你甭管我在哪,我揍它,是吧,然后服务器那边,大家注意,很实在啊,行,然后,就看一下,你多少血,它多少血,给揍,是吧,然后,所以你们看,可以看,有的时候,你看那外挂,是什么情况,就是人在那边手舞足蹈,你也不知道干嘛呢,这人干嘛呢,在那手舞足蹈,是吧,就接着往前走走走,特别,你看特别来回,它就在那掉血,是吧,原因就在于是这么个原理啊。
  • 但是听我说啊,外挂这个东西,你看服务器刚开始,是不是不理你,对吧,它给你处理啊,你这些处理是,你在这地底下走,是吧,那怪就在上面,看不见你在地底下走啊,我们那会玩魔兽的时候,就经常见到什么呀,挖矿,我不知道大家有,有同学可能玩过魔兽,那个现在可能已经很和谐了啊,当年采矿的时候都得抢,有一些联盟,和部落互相抢,先给你干死,我再抢,是吧,然后经常有人,你会发现什么呢,你就听这矿,当,当,当,被人打着呢,那在哪呢,这人,一看,它在墙里头干呢啊,这就属于使外挂,是吧,但是你会发现,服务器很实在,它告诉你我在墙里面那个坐标,那服务器就给我在那了,明白吧,它也不处理啊。
  • 但是我告诉你,这个时候日志服务器就非常重要了,因为什么啊,你在跟服务端处理的每一个业务,服务器这边,都会先跟日志说,就比如说,我要打谁一下,服务器就会告诉日志,谁谁谁,要揍谁谁谁,使了个什么技能,那也就是说,他们就相当于什么呢,就相当于把这信息发给日志服务器,它们都会发送给日志服务器,你做的交易啊,你的任何操作,明白么,都会告诉日志服务器啊,所以,那你就知道,日志服务器是非常繁忙的,它要把所有的数据都记录下来,那你想想,量是非常大的,同时并发量是多少,用户量有多大,对么,那我的,你想想,我在往数据库里存的时候,效率是不是就一定得保证,那这个时候你怎么干啊,日志服务器。
  • 那服务器,数据库是另一台机器,日志服务器要连接数据库,就被我们的jdbc连上,是吧,那么给它发的时候,那么你就得批操作,而且呢,一次请求过去,一个事务,把这一堆数据进去一次,是吧,那一秒钟,好几千条,好几万条的数据量,你想想你怎么插入,一秒钟,是吧,那这数据量就非常大了啊,所以这个时候,我们说批操作就非常非常重要啊,那个时候,我们在写的时候,都得这么干啊,你要再一个事务一次,一个事务一次,那就赶死你,是吧,你根本就满足不了什么呢,这些集体向你发送日志的这个,这个需求了啊,那么说这么多吧。

返回自动主键:JDBCDEMO_generatedKeys.java

  • 这个批操作,我们说完了以后呢,下面来说一说,这个返回自动主键啊,返回自动主键,一般用在什么地方比较多呢,这个啊,我们现在知道咱们现在往一张表里面插数据的时候,主键一般是不是由系统生成,比如说,用序列生成,对吧,那比如说,我希望在插入一条记录的同时,把那个主键值得到,怎么办,因为你看,比如咱们往一张表,往这个username表里插数据吧,比如我插了条用户信息进去,那我是不是可以把用户名,昵称,这些信息给它,唯一给不了的是不是就是id,对么,但我现在插进去,希望得到这个id怎么办,比如这id是用序列生成的,我们是不是可以再把它查出来。
  • 那怎么查啊,那我先insert进去,我再按照这个条件,把这条记录查出来,看看你id是多少,不是不可以,但这样的话,你是不是至少,至少执行两条语句,对么,你先insert插进去了,然后你再select什么用户名等于这个啊,密码等于这个,查一下id是多少,是吧,这样的话,能查到,还有别的办法么,有人说我可以这么干,我不查userinfo表了,我把它这条记录插进去以后,我查序列,我查序列的currentValue,序列不是有两个伪列,一个是nextValue,一个是currentValue么,currentValue不是获取的就是你最后生成的那个数么,你既然用序列生成那个插进去的那个值,那我们再得到它不就好了么,那我告诉你,这个更不行,为什么呢,你怎么能够保证,你插进去的那条数据啊,这条数据以后,你再获取这个序列的当前值的这个,这个过程中,别的线程,没往这表里插入记录呢。
  • 如果有别的线程,也往这张表里插入一条记录,它也从这序列里面获取这个值,那你拿到这个序列是什么值啊,还是你那条记录了么,就可能不是了,对吧,那又有人提供了一个解决方案,说那我这样,我先查序列的nextValue,我先单独select,序列nextValue,from 伪表,我先单独拿出这个序列的值,拿到以后呢,我把它作为id,插到表里去,然后这个值就得到了,对吧,但其实,你会发现不管你怎么做,你都是两条sql语句,对不对,插入一条,你是不是还有一个获取主键,获取那个你插进去那两条记录里面id那个字段的值,是不是还有一条记录,还要有一个这个,这个查询语句,是这样的吧。
  • 那大家注意,那有没有办法说我这insert时就同时就拿到它,有,那我告诉大家,jdbc就给我们提供了什么呢,相应的模式就是返回自动主键,就是你可以在插入一个字段,就是插入一条记录的同时,你得到某一个字段对应的值啊,那这样的话将来就比较方便,那说我什么时候需要说,插入一条记录以后,还要把它那个id值得到,你们想,有时候会有这么一种情况,比如说,我往员工表里插数据,往部门表里插数据,这是员工表emp(emp表在JDBCDemo6分页查询功能时已经准备过了。),这是部门表dept,准备工作如下:

CREATE TABLE dept_Van(
  deptno NUMBER(2,0),
  dname VARCHAR2(14),
  loc VARCHAR2(13)
)

DESC dept_Van

INSERT INTO dept_Van
(deptno,dname,loc)
VALUES
(10,'ACCOUNTING','NEW YORK');
INSERT INTO dept_Van
(deptno,dname,loc)
VALUES
(20,'RESEARCH','DALLAS');
INSERT INTO dept_Van
(deptno,dname,loc)
VALUES
(30,'SALES','CHICAGO');
INSERT INTO dept_Van
(deptno,dname,loc)
VALUES
(40,'OPERATIONS','BOSTON');

SELECT * FROM dept_Van

COMMIT
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

  • 那比如说,我先干什么呢,往这部门表里插一条数据的同时,顺便往员工表里面插一条员工信息,就是对应的,这个部门还有一个员工,那这时候就出现了一个问题,比如说啊,我输了一下部门的信息,也输入了一下它下面那个员工的信息,是吧,往那插入数据,那比如说,我现在先往部门表dept里插数据,有(deptno,dname,loc),三个数据,我这个部门叫IT部,是吧,那等等信息,那id,即deptno怎么得到的,是不是就可以靠序列得到,对吧,比如我序列得到的值是1,假设我把这条记录插进去了,是吧,那关键的问题是,一会,比如说,我往员工表emp里插条记录,它有它自己的id,然后这个是什么,张三,是吧,那张三它在哪个部门呢,它那个部门号,它是不是得把这个值,部门表dept里的id值搁到员工表emp里,做为外键,这外键就是相当于是那部门的deptnumber,那个字段么,是吧。
  • 那这个时候,你就想想,那我是不是,至少我在往dept表里插入一条记录以后,我得到这个dept表里这个id,这个主键的值,那你得到这个值以后,你是不是才能把这个值插到员工表里,作为外键,对么,那这个时候你怎么办,你会发现,你是不是就需要在,插入部门表的同时,就需要把部门表的这个id值得到,这样的话,等你现在往员工表里插数据的时候,这个deptnumber的值就有了,如果没有的话,就麻烦,你要先插一条部门信息,是吧,部门信息插进以后,再select,把这部门号查出来,再把这个值作为,员工表的外键插进去,那你这样的话,是不是得执行3条语句,对不对。
  • 所以由此可见,我们是有这种需求,就是说,在插入一条记录的同时,把这个主键字段就得到的这种情况的,是吧,而我们之前也说了,主键基本上都是什么呢,这个让系统生成,一般都不是我们人工维护的了,是吧,尤其你将来,要是用序列生成啊,那我们就可以靠自动返回主键那种机制,在你插入的同时就应该把这个值得到啊,那怎么做呢,你们来看,它给我们提供的模式是这样的啊,没有使用返回自动主键的时候(方法一),

1.通过序列产生主键(Oracle):
在Oracle中,建议主键通过序列获得:
String sql = "insert into dept (deptno, dname, loc) values(dept_seq.nextval,?,?)";
如果单表操作,不需要返回刚刚插入的主键值;如果关联操作,需要将刚刚插入的主键值返回。
2.方法一:先通过序列的nextval获取序列的下一个值,再作为参数插入到主表和从表:
获得主键:String sql = "select dept_seq.nextval as id from dual";
插入主表:String sql1 = "insert into dept(deptno, dname, loc) values(?,?,?)";...ps.setString(1, id);...
插入从表:String sql2 = "insert into emp(empno, ename, deptno) values(?,?,?)";...ps.setString(3, id);...
简单,但需要额外多一次访问数据库,影响性能。
3.方法二:利用PreparedStatement的getGeneratedKeys方法获取自增类型的数据,性能良好,只要一次SQL交互。
sql = "insert into dept (deptno, dname, loc) values(dept_seq.nextval,?,?)";
//stmt的第二个参数是GeneratedKeys的字段名列表,字符串数组
stmt = con.preparedStatement(sql,new String[]{"deptno"});
stmt.setString(1,"IT");
stmt.setString(2,"Beijing");
stmt.executeUpdate();
//获取主键值所在的rs
rs = stmt.GeneratedKeys();
rs.next();
//从rs中取出主键值,从表示用
int deptno = rs.getInt(a);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

  • 不理想的话方法一,你要执行,这么几条sql语句,首先,你看它这是干嘛,String sql = "select dept_seq.nextval as id from dual";,这是不是从一个序列里,调nextval,现在等于说,from 伪表, 等于说,你是不是单独先从这个序列当中,获取了一个值,然后得到这个值以后呢,你是不是再把这个值作为员工表的主键插进去啊,String sql1 = "insert into dept(deptno,dname,loc) values(?,?,?)";,作为,再作为员工表的外键,是不是才把那条记录插进去,String sql2 = "insert into emp(empno,ename,deptno) values(?,?,?)";,也就是说,那你至少得有一个单独获取,部门表这个主键字段值的这么一个操作,是吧,那这个不理想。
  • 那我们的方式二呢,就是什么呢,可以在插入的同时进行,那这个怎么做呢,大家来看这个,比如说,我写这条sql语句,sql ="insert into dept(deptno,dname,loc) values(dept_seq.nextval,?,?)";,你看这都写好,然后这个values,值是不是就是用那个序列的下一个数,是吧,然后后面呢,是两个问号,就给值,然后呢,我创建preparedStatement,stmt = con.preparedStatement(sql,new String[]{"deptno"});,因为上面不是还有动态信息么,所以这个地方我们是不是应该使用preparedStatement,对吧,那么你这个地方呢,我们就把这个sql语句给进去,同时大家,原来我们是不是没有传过第2个参数,原来咱们创建PreparedStatement,我们是不是把这sql语句传进去就行了,是吧,那实际上PreparedStatement还支持第2个参数啊,那它第2个参数是一个字符串数组,这数组里面,每个元素是什么呢,就是你希望在执行完这一条,要插入的insert,预编译语句以后,得到那条记录里面哪个字段的值,你就在这个第2个参数里指明就行了。
  • 那咱们一般情况下,只获取主键,就是咱们这里面给一个字段的名字就行了,也就是说,我在插入这条记录的同时,我希望得到这条记录当中部门号的值,如果你还希望连dname也得到的话,怎么办啊,你这个数组里面呢,再传一个元素,就是第2该参数都是传的那个字段的名字,明白吧,就是说,我在执行完这条sql语句以后,我希望得到那条记录当中,哪个记录的值,那当我这么指明完了以后,你看啊,咱们往这个里面,给这两个问号?,是不是对它设置好值,stmt.setString(1,"Research");stmt.setString(2,"beijing");stmt.executeUpdate();,然后executeUpdate,这时候,是不是这条记录就插进去了,是吧,插到表里去了,插到表里去以后呢,这个时候就调用,rs=stmt.getGeneratedKeys();,它有一个方法叫getGeneratedKeys(),就是获取生成的键,就是它那个GeneratedKeys,就是生成的键,是吧。
  • 那你就理解为就是,获取什么呀,这个,这个生成这条记录里面,你指明那个字段的值,那刚从咱们,是不是一执行update等于把这条记录,insert这张表里去了么,是吧,然后呢,因为我跟这个preparedStatement说了,我要你插入一条记录以后,我要这个部门号的值,所以你在调getGeneratedKeys();,这个方法的时候,你可以得到一个结果集,而这个结果集就是对应的是你刚才,你要指定的那个字段对应的值是什么,它就可以把它返回回来啊,而这个里面只会有一条记录,因为你只插了一条记录进去,而那个记录里面,这个结果里面只有一个字段,就是你上面指定的字段,就是你要这个字段的值啊,它就返回给你了啊,rs.next(); int deptno = rs.getInt(1);,如果我们要写的话,就类似于这个样子,我给大家分开,在sqldeveloper,这块给大家演示,大概是什么意思啊,就好比说啊,我应该没有这个序列吧,创建一个啊,

CREATE SEQUENCE seq_dept_id
START WITH 1
INCREMENT BY 1
  • 1
  • 2
  • 3

  • 就好比这相当于什么呢,

INSERT INTO dept
(deptno,dname,loc)
VALUES
(seq_dept_id.NEXTVAL,'IT','BeiJing');
  • 1
  • 2
  • 3
  • 4

  • 那这时大家注意,当我一执行这条语句,是不是就相当于我就把这条语句执行了,那我们在这stmt.executeUpdate();,生成preparedStatement的时候,什么呢,我告诉你了,我要你插入stmt.setString(1,"Research");stmt.setString(2,"beijing");,这条记录以后,是不是要deptno这个字段的值,那就意味着相当于什么呢,我在执行这条语句以后,我要你插入这条记录里面dept这个字段对应的值,那就相当于,这条记录插进去以后,它就自动干了一件事情,什么事呢,就是把这个deptno字段的值获取回来了啊,那就相当于是那个值啊,那这个时候大家注意,那当我们插入完毕以后,这块stmt.executeUpdate();,执行完毕以后,你一调rs=stmt.getGeneratedKeys();,那大家注意,它会返回什么啊,返回给我们一个结果集,而这个结果集是谁呢,就是你刚刚插进去,这条记录当中,对应哪个字段啊,deptno这个字段的值,就相当于这个当中的一个结果集回给你了,明白么。
  • 那也就是说,这个结果集里是不是就只有1行1列,所以就是说,你在这调了一下,rs.next();,也就是说,我们就相当于把那个指针,指向了第1条记录,发现它是有的,是吧,然后呢,你是不是获取第一个字段,rs.getInt(1);,因为这是一个整数吧,你调getInt的话就相当于就把这个数字1就得到了,就把这个,相当于把这个指针的值就给取到了,对吧,但如果大家注意啊,我在这写了两个字段,除了这个deptno之外,我写一个dname,比如说,我还写了一个dname,那就得到了,deptno和dname这两个字段,对应的值,当你调stmt.getGeneratedKeys(),你就得到了这两个,明白么,得到着两个呢。
  • 然后呢,你要获取第一个字段的值呢,就是deptno所对应的值,获取第2个字段对应的值呢,就是dname所对应的值,当然,我们现在要这个是没有意义的,我们其实只有deptno这个值是靠序列生成的吧,所以我们其实要deptno就行了,能理解吧,那么呢,咱们来做个测试吧,大家来先给我一起做个事情,一起来创建个序列,再往那个部门表加个序列,如果我没记错的话,员工表是有序列的。下面我们在ojdbc项目中,src/main/java/demo包下面,建一个类,叫JDBCDEMO_generatedKeys,返回自动主键。
  • 首先,还是大家先,基础工作大家都写好,try-catch-finally块,DBUtil创建连接,关闭连接等工作都做好啊,那么现在,比如说,我们现在就往部门表里插入一条数据,然后在插入的同时呢,把它那个主键值得到,那我怎么做啊,首先咱们写一条sql语句,然后sql语句就是insert into dept,部门表是吧,然后呢,我们说咱们给值(deptno,dname,loc),然后呢,values,这块是,(seq_dept_id.NEXTVAL,?,?);,是这样的吧。

String sql = "INSERT INTO dept_Van "
		     + "(deptno,dname,loc) "
		     + "VALUES "
		     + "(seq_dept_Van_id.NEXTVAL,?,?) ";
  • 1
  • 2
  • 3
  • 4

  • 然后,都做完了以后呢,我们是不是就可以创建preparedStatement了,是吧,创建preparedStatement的同时呢,传两个参数,传两个参数啊,第一个参数就是那个sql语句,第2个呢,就是一个columNames,什么意思,就是字段的名字,在这new一个String的数组,然后呢,这里面的值呢,就比如说,我是不是就只要deptno这个字段的值就行了,是吧,生成这个字段的值就行了,那我就只要这一个,就告诉你,PreparedStatement ps = conn.prepareStatement(sql, new String[] {"deptno"});,当你这条sql,你执行一次以后,你不执行一次插入么,获取当你插入那条记录当中对应的部门号的值啊。
  • 那这个时候,我们来开始给它设置内容啊,比如setString,第一个问号,比如说是IT,ps.setString,第2个问号,是Beijing是吧,然后呢,ps.executeUpdate();,执行完是不是就插进去了,对吧,

PreparedStatement ps = conn.prepareStatement(sql, new String[] {"deptno"});
ps.setString(1,"IT");
ps.setString(2, "Beijing");
ps.executeUpdate();
  • 1
  • 2
  • 3
  • 4

  • 插入进去以后,我要干嘛呢,获取插入的数据中指定字段的值,然后,我怎么得到呢,就调那个ps.getGeneratedKeys();,然后就那个得到了,我得到一个结果集,而这个结果集应该是只有一行一列的,为什么呢,因为你执行ps.executeUpdate();,这条语句,这条语句是不是只会往表里插一条数据,是吧,然后你还只要这一个字段的值,那也就是说,我们会获取到什么呢,你查入那条记录当中,deptno那个字段的值啊,这个时候,我们就调rs.next();,如果你不放心,你可以if(rs.next()){},确定它有那条记录,是这样的吧,那你插入的话,应该是没问题啊,如果有的话呢,我们调int id = rs.getInt(1);,为什么调getInt啊,因为部门号是一个整数,而这个地方,我们写1,就是第一个字段,这个地方注意,不要写字段名,因为它不是以字段名的形式,返回给你的啊。虽然我这个地方,说我要的这个字段啊,但是这个地方不要用字段名啊,直接用这个位置,这是获取这个第1个字段的值是吧。
  • 那得到了啊,int id,也就是说我们插入一条记录了,比如说,插入的部门id是,加上id,System.out.println("插入的部门ID是:"+id);,那我们来看看啊,执行,现在大家看插入的id,部门号是几啊,是2,为什么是2啊,因为刚从我在sqldeveloper测试的时候,是不是把1用了,是这样的吧,那这个时候,我们来sqldeveloper查这个部门表的话啊,from * from dept,大家看这个是不是2,也就是说,我刚才是不是,确定把这条记录插进去了,而且,我是不是同时就把这个id值得到了,将来你拿到这个id值以后呢,你是不是就可以用这个id值,接着往员工表里插数据了,那他们的部门号,是不是就可以对应的是2,明白这意思吧。
  • 所以就是说,将来你要是想在插入的同时,获取某一个字段的值的时候,你就创建PreparedStatement,同时指定一下,我想要你插入那条记录当中哪个字段的值就可以了啊,另外,我告诉你们啊,等你们后来使的很多框架啊,它们都自带什么呢,这个功能,到时候,你只需要加一个配置文件就行了,是吧,我帮你自动获得这个主键啊,那其实你得知道,底层实现,大概都是这么干啊。
代码实现:
package demo;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

/**
 * 返回自动主键
 * @author soft01
 *
 */
public class JDBCDEMO_generatedKeys {
	public static void main(String[] args) {
		Connection conn = null;
		try {
			conn = DBUtil.getConnection();
			
			String sql = "INSERT INTO dept_Van "
					     + "(deptno,dname,loc) "
					     + "VALUES "
					     + "(seq_dept_Van_id.NEXTVAL,?,?) ";
			
			/*
			 * 创建PS的同时指定执行该PS对应的SQL语句后
			 * 要得到插入记录中指定字段的值
			 */
			PreparedStatement ps = conn.prepareStatement(sql, new String[] {"deptno"});
			ps.setString(1,"IT");
			ps.setString(2, "Beijing");
			ps.executeUpdate();
			//获取插入的数据中指定字段的值
			ResultSet rs = ps.getGeneratedKeys();
			rs.next();
			int id = rs.getInt(1);//获取第一个字段的值
			System.out.println("插入的部门ID是:"+id);
			
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			DBUtil.closeConnection(conn);
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

2.高内聚低耦合应用

在WebServer中存在的问题

  • 那么现在呢,我们来看看,怎么去处理这个WebServer啊,注册的功能,RegServlet,咱们之前在做这个注册那个功能,RegServlet啊,在service方法当中,要求先判断用户存不存在,要不存在,是不是才执行插入,是这样的吧,那这个时候,其实我告诉你们啊,这个其实,写没啥,就加个select语句,看一下有没有结果,是这意思吧,要是有的话呢,就是那个,跳一个页面,就是该用户已存在,要是没有的话呢,就直接就执行注册了,插入就完了,是吧,这个本身没什么啊,那关键的问题在于啊,会发现一件事情,就是什么呢,这个Servlet,大家注意啊,它的这个功能,就是让它干的活太多了,这已经违背了咱们低耦合的原则了啊。
  • 也就是说,你会发现这个service,它是不是第一步首先要获取得到这个用户,这个信息,是吧,还要负责查数据库,还要负责看看数据库里面,数据都对不对,有没有,是这样的吧,也就是说,它现在是不是既处理业务逻辑,也要处理和什么呢,数据库之间的沟通,对么,那这个时候,在实际开发当中,我们就要干嘛呢,拆分了啊,这个业务要拆分了,我们应该做到什么呢,访问数据库就专门有一个类,你就管跟数据库打交道去,就好了,这样的话呢,我们在这里边,就只管业务逻辑,也就是说,数据怎么存在数据库里,怎么从数据库里查出来,这些都不是我关心的事了。
  • 我只关心,拿到数据以后,它有没有,数据对不对,对吧,对,我怎么处理,不对,怎么处理,我是不是只管业务操作,对吧,具体说怎么存在数据库里,那我再交给另一个类干,而不是说一个事,我全干,我又管插数据库,又管查数据库,还管怎么啊,它跟数据对不对,还得想着,怎么跟用户跳页面,还得想着怎么从用户那获取数据,全让我一个人干,你这个时候,你这个业务耦合性太高了,是吧,它有从用户那得到信息的地方,有处理你自己业务的地方,还有处理数据库这样的地方啊,那我们一般不这样干啊,刚开始,我们这样写,没关系,咱都先写在一起,没事,能运行,代码也能运行,也挺好,说我也觉得挺简单,我还能看得懂,是吧。
  • 但是你们得知道,将来在实际开发当中,首先第一点,不是你一个人写项目,是吧,很多个人一起在写项目,那这个时候,大家注意啊,项目经理一般再给我们做拆分的时候,有两种,竖着拆,横着拆,横着拆是按层拆,竖着拆是什么呢,按业务拆啊,就直接按流程拆,就比如说,注册功能归你写,登录功能归他写,那这个时候,整个这一个流程是不是都是你写,那这是竖着来,横着来是什么意思呢,一人一个层次,你只管业务逻辑,那个人只管所有数据库操作啊,那我们有这么两种拆分的方式啊,所以,但是至少我们会发现,不可能所有的功能,一个类全搞定,是吧,你这样的话,你这个,我们做不到高内聚啊,做不到高内聚,就会导致一个什么啊,你将高耦合,其实是这意思,是吧。
  • 就是说你那个,这个功能之间的耦合性太高,你想改一下,你就会经常会出现一件事,就是我们程序里面经常讲的一句话叫,迁一发动全身,是吧,你发现你这改一下,那改一下,那也改,那改,那改,全都改,最后来一句,算了,我还是重新写一份得了,是吧,那就很不理想,就是将来你后期的维护成本会很高啊,我们其实拆的越散越好啊,这东西其实怎么理解呢,变相理解,咱先不说,它的这个,这个思想啊,或者怎么的,得到的这个,这个经验啊,咱先不吹这些事,是吧,你就考虑一个问题,写程序告诉大家,很多情况下,跟搭积木是一个道理。
  • 搭积木你是一个大块利用率高,还是一个小碎块利用率高,小碎块利用率高,对吧,你弄一个大块的,你想想,那个就是一个什么呀,低内聚的,是吧,就是大家注意,你这一块,那么老大陀,是吧,那你说,你再想把它拆,你又拆不了,它又不能用了,怎么办,那是不是就很麻烦,那我们说小块,如果你想都能把它拆成小块,我是不是也能搭出这种结构来,那将来,我哪个地方是不是都可以换,想怎么换是不是都可以换,对吧,而且你换的时候,代价是不是很小,如果你这是一陀,一个整体,你要是不能用,要么就都不能用,要么就用上,对不对,就跟你所看到的,你会发现咱们现在基本上,你所有用到的东西,都是高内聚,低耦合的。
  • 什么意思啊,随便举例,汽车,发动机是一陀么,不是,一个发动机里面是不是也分轴承,什么各种零部件一大堆,组合成一起,你们不知道什么呢,一个发动机里面上千个零件,它为什么不用一个零件就搞定了,你坏一个怎么办啊,全换了,还是说,我坏一个就换这一小块,哪个好,对么,所以为什么就是说,你们看你知道的任何的这种,制造一个商品,都是一大堆的零件组在一起的,是吧,就是让你各个环节,你们之间是配合工作,但是每个,每个部件,跟每个部件之间,都没有一个必然的耦合关系,我没你,换一个,照样能用,是吧,那这样的话,你将来是不是修的话,成本低啊,对么,否则的话,你说你汽车爆一个胎了啊,换辆车吧,能么,不能这么干,我现在就是说,爆一个胎,顶多换一个胎,就完了,我连轮毂都不用换,我就换一个胎,对么。
  • 如果要不是的话,你想想,这个整个都是一体的,你坏一个东西,你就都得换,成本是不是太高了,对吧,所以我们做什么事情,现在不都是拆散,能拆的越散越好,你虽然看它非常复杂,但你会发现,将来你的维护成本会很低,清楚吧,所以我们程序里面,也是这个原则啊,只不过我们程序,就是没有那种具象化的东西,没有那个具体能摸着着那些玩应,都是代码,是吧,你可能不感觉,但其实原理都是这个意思,都是能给它尽量拆碎,就给它拆碎了,清楚了吧。我们在给它想办法组在一起啊,所以在这呢,我们做一下拆分啊,那我们来把它们,RegServlet啊,进行一个拆分的工作。

数据持久层:DAO

  • 也就是说,我们把什么呢,所有这个对数据的增删改查的工作,就是跟数据库打交道的东西,咱们可以单独写在一个类里啊,然后呢,把这些逻辑呢,我们给它放到另一个类里边去干啊,那么在这个地方呢,我们就会抽象出来一个层次,就是将来在写项目当中,我们会专门有一个层次,那个层次叫作什么呢,DAO啊,它就是什么意思呢,就是Date Access Object,叫作什么呢,数据连接对象啊,或者叫数据访问对象啊,那么大家注意,DAO,听我说啊,不是针对的是具体指的某一个类,它是指的是某一个层次,就跟我们说,咱们不是可以把一堆类,比如这些类都是负责处理业务的,那我们叫业务层,就是这个层次里面的所有类都是处理业务逻辑的。
  • 再扩大一个层次,这个层次叫作什么呢,数据持久层,就是这里面的类,都是负责什么呢,把数据存到数据库里,从数据库里把数据查出来,就是它负责和数据库打交道的,那么这个我们一般叫数据持久层,而DAO就处在数据持久层啊,什么叫数据持久化啊,还记得么,我们在说对象序列化的时候,说过持久化这件事吧,持久化就是什么意思啊,就是把数据持久保存,你写硬盘上,不就能持久保存么,断电还有,我跟你说在内存里的东西都是不可靠的,断电就没了,你要想一直保存,就得放到硬盘上,放到硬盘就能持久保存,就长久保存,我们这个过程,就类似你把数据写在硬盘上的那个过程叫持久化。
  • 数据持久化,说白了,就是把数据写硬盘上,这个过程叫数据持久化,是吧,那什么叫持久层啊,就是这个层次里面的代码,都是用来做数据持久化的,说那还干嘛要写那么多类啊,你得知道,你将来的数据也分很多种啊,有用户,比如说你写个论坛,它包含的有用户的数据,帖子的数据,版块的数据,那是不是你得分为不同类型的数据啊,那你不能把这些东西都写在一个类里啊,那我是不是可以有专门处理用户的类,专门处理帖子的类,专门处理什么的类,对不对,那他们是不是都是负责把数据写硬盘上,那它们是不是都是在数据持久层里面,只不过,我们处理的数据不一样而已,能理解吧,但它们都是DAO。
  • 那什么叫DAO,什么叫这个数据连接对象呢,那么你就得考虑这个问题,业务层,就比如说我这个业务逻辑,比如说啊,用户把一堆的数据,注册信息交给我们业务层了,就是它把用户名啊,密码啊,邮箱啊,什么都给我们了,我是不是在业务层里,首先可以先看看,它那东西都对不对,这是我正常业务应该处理的功能,先验证你的数据,符不符合要求,对不对,比如都符合,那我是不是需要把它存进来了,然后怎么存啊,咱们是不是在之前在servlet里直接就连数据库,卡,插进去了,那等于说,我既负责业务,又负责操作数据库,是吧,那现在不这样干了,咱把操作数据库这件事干嘛呢,交给另一个类干。
  • 那就会带来一个问题,我怎么把数据给你,调方法呗,是吧,也就是说,你这个DAO,是不是可以写一个方法,你这个类写一个方法,就是你把用户的信息都给我,我负责连上数据库插进去,对么,那就得考虑一个问题,我在定义这个方法的时候,我的参数怎么定义啊,你有用户名,有密码,昵称,有什么Email,什么有一大堆呢,那我定义多少个参数啊,定义一个,说怎么就定义一个呢,你这一堆,还不都是用户信息么,对么,一个用户不就包含这么多信息么,那我是不是可以先定义一个类,叫作Userinfo,你有几项,我就有多少个属性,这样的话,我是不是每new一个Userinfo实例,都可以表示这堆信息,你直接都把这个对象交给我,我负责帮你把这个对象里的数据,帮你插到表里去,对吧。
  • 这样的话,好处在于什么啊,将来你想查这个用户的信息呢,我是不是把这些查着以后,也是不是组成,组成一个Userinfo对象给你,也就是说,那这样的话,它的好处在于,业务层对于这个数据库的操作,是不是就可以完全的面向对象化了,就是我都是以传对象的形式传递这些数据,而不用分开的一个一个字段的值都给你,那很散很乱的,明白吗,所以就是说,DAO这个层次的好处在于什么呢,让我们跟数据库之间的操作,因为数据库本身不具备什么面向对象化这个事,它就是一张表,里面一个数据,对吧,可我java,咱们的程序员已经习惯了,我们在java中保存一组数据的话,是不是都是用对象的形式保存,一切皆对象么,对吧。
  • 那我怎么能够让我们感觉是,我是在操作对象,那我们就通过DAO来干,它所谓叫作数据连接对象的意思就是说,可以把我们业务层给我们的对象转成数据库的数据,插到表里去,也可以把数据库里面的那些数据查出来以后呢,把它组建成java中的对象,再返回给业务层,也就是说对于业务层来讲,你存的时候存的是对象,取的时候,我也是以对象的形式给你,就好比你感觉跟你这对象就序列化了,以后再把它反序列化回来的感觉似的,明白么,但实际上,我们不是自己把它转成字节写文件里去,而是把它交给了数据库吧,明白吧,DAO就负责帮我们做这件事情啊。
  • 所以Dao,它叫做数据连接对象,意思就是说,这个DAO这个类的作用就是,让我们业务层对于数据库的操作,变成什么呢,完全面向对象化的操作啊,就是咱们来回来去的存数据啊,查数据,改数据啊,都是以对象的形式传递,也就是说,你不用再自己去想着怎么去弄sql语句,搞到数据库里去了啊,就是全部都交给我干,也就是说,把你这个业务层次这个活,屏蔽掉,是吧,那咱们之间更加专注,你只需要关注你的业务逻辑就好了,而我(DAO)来关注怎么样呢,数据和java中的对象之间,是怎么互相转换的啊,然后呢,我来操作数据库,明白吧,我们俩把这个层次给它分开,让它的条理性更好,不是说你一个人干不了,是吧,而是说,你一个人,全让你干的话,你将来维护成本太高,明白吧。
  • 我想随便换一下,是不是你整个这个类都要动,对么,现在我只需要换一个层次里面的某一个类的,某一个方法,我就能搞定这事,我就没必要换那么多,那么多东西啊,那这个就是,我们说要分层的好处,而且我们为什么要再抽象出来一个层次,明白吧,就是这个意思啊,那咱们呢,现在就来写一个DAO,我们先看看啊,基础的DAO,其实无非就是对于用户这个对像,对于我们来讲吧,咱们现在是不是只有用户表,咱们只有这一张表,那咱们是不是,至少可以针对用户表,我就可以有一个Userinfo的DAO啊,就是这个DAO只是负责什么呀,操作用户信息的,那你操作,你看现在咱们写了这么多例子以后,我们现在对于用户表都有哪些操作啊,增删改查都有,是吧。
  • 所以告诉大家DAO里边,正常情况下的功能,增删改查,肯定也都得有,因为你将来业务逻辑层,肯定得需要用到,增删改查数据么,是吧,所以我们把那些信息给它提供好啊,那么大家呢,跟我一起呢,我们来写一写啊,咱们直接在WebServer里面,做这个操作了啊,咱们在WebServer里面,我们再新建一个包,叫作dao,然后在这个包里,新建一个类,叫UserinfoDAO。

3.重构WebServer项目之数据持久层一

  • 然后,来到WebServer项目下UserinfoDAO类。比如说吧,咱们在这个userInfoDAO里面呢,我先写一个功能啊,比如说就是保存用户信息的功能啊,那我们对外,咱们给业务逻辑,暴露出来的都是对象,也就是我们要把这个,我们实际上数据库里面每一张表,将来你这个项目当中用的每一张表,都应该用java的一个类去表示,我们说过表,咱们是不是可以用类表示,你表里有多少列呢,我们是不是就可以有多少个属性,你的每一条记录,我们是不是都可以,用一个具体的实体来表示啊,所以就是说,咱们将来在写程序的时候,都是这个样子,我们要为每一个表,将来这个项目中的每一个表,都要怎么样呢,对应着一个类,我们这个类一般叫实体类啊,就先写这么一个类啊。

代码分析:Userinfo.java

  • 那比如说,咱们这个啊,数据库里面是不是,现在有userinfo这张表,对吧,那我们再对应什么呢,要有Userinfo这么一个类啊,那么我们来再建立一个包,叫entities,包里再建一个类,叫UserInfo。在这个UserInfo类中:那比如说我们这张表里面对应的一些字段,那在这呢(在这个UserInfo类中),我们就要定义相应的属性啊,比如说,有什么呢,int型的id,String型的username,我们将来在定义这个实体类的时候,一般情况下,我们的这个类的名字要跟你表名一样,当然我们java中,你还是符合java的命名规则,就该大写的地方大写,就行了,然后,里面的这些属性名,应该是和你那张表里面对应的字段名一样,清楚么。
  • 我们那表里面,不就id,username,password,nickname,account,是吧,那在这块呢,我们也定义成相应的这些名字啊,然后对应的类型也是一样的啊,username,password,然后,nickname,还有account。然后定义一下这构造方法啊,首先先定义一个默认无参的,能够快速创建对象,然后我们再定义一个全参数的,以及get,set方法,当然我们也可以把toString自己加上啊。见代码实现:Userinfo.java。
代码实现:
package entities;
/**
 * UserInfo类用于表示userinfo表
 * 其每一个实例用于表示userinfo表中的一条记录
 * @author soft01
 *
 */
public class UserInfo {
	private int id;
	private String username;
	private String password;
	private String nickname;
	private int account;
	
	public UserInfo() {
	}

	public UserInfo(int id, String username, String password, String nickname, int account) {
		super();
		this.id = id;
		this.username = username;
		this.password = password;
		this.nickname = nickname;
		this.account = account;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getNickname() {
		return nickname;
	}

	public void setNickname(String nickname) {
		this.nickname = nickname;
	}

	public int getAccount() {
		return account;
	}

	public void setAccount(int account) {
		this.account = account;
	}
	
	public String toString() {
		return id+","+username+","+password+","+nickname+","+account;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

代码分析:UserInfoDAO.save(…)

  • 这个写好了以后呢,那么现在,我们就回到UserInfoDAO来,我们来加一个方法,public boolean save(UserInfo userInfo) {},那么在这个地方呢,实际上就相当于,到时候这个业务逻辑层会把这个,它想保存这个用户,那个信息呢,变成一个UserInfo的对象交给我们,那在这个地方呢,这个save()方法里,我们就可以把它插到数据库里头去了,也就是说现在和数据库打交道的是由UserInfoDAO来干了啊,那就不再交给,原来我们的Servlet去干了啊,那么在这个地方,我们怎么做呢,首先,这个save方法里面,其实就跟之前一样了,首先Connection conn=null;,是吧,声明一个连接对象。
  • 然后呢,try-catch-finally和DBUtil关流,try里面DBUtil建立连接等基础工作,如果之前这块没有成功,出来以后,我们return false,告诉你没有保存成功,那如果我在try这个里面呢,拿到它(连接)以后,咱们是不是就可以开始往里面插数据是吧,那这个时候首先创建sql语句,

String sql = "INSERT INTO userinfo_Van "
		     + "(id,username,password,nickname,account) "
			  + "VALUES "
		     + "(seq_userinfo_id.NEXTVAL,?,?,?,?)";
  • 1
  • 2
  • 3
  • 4

  • 先是insert into 到 userinfo,然后呢,(id,username,password,nickname,account),然后呢,这个地方是values,然后呢,这个地方呢就是,第一个是seq_userinfo_id.nextval,是吧,然后呢,四个问号啊,然后呢,我们就开始创建PreparedStatement,是吧,它等于Connection点prepareStatement,然后把sql语句给进去,那么我们知道一点,实际上,由于上面的id是不是我们提供的,实际上就是将来它(客户端)给我的时候,就可能没有id这项啊,但是当我们插入完以后呢,这id就有了,我们把id呢,就也还给它设置回到这个业务层传过来的userinfo对象上,让userinfo对象,它去表示这条记录,一个包含完整信息的userinfo实例,以备在注册功能之后,有一些其他的相关联的操作需要用到这个userinfo对象的信息。
  • 这样就表示我插入记录完毕,并且呢,你id也有了,因为现在userinfo(从RegServlet)给我们(传过来)的时候,实际上是不含有id的,因为id是我在插数据库的时候是不是管序列要的,但是剩下的这四项问号,是不是都是应该在传过来的userinfo这个地方,它会给我的,将来啊,那由于它没有id,所以这块我们干嘛呢,new String[]{"id"},这块我们要什么呢,返回自动主键,在插入的同时得到序列自动生成的这个id的值,是吧,PreparedStatement ps = conn.prepareStatement(sql,new String[] {"id"});
  • 然后呢,这个时候,preparedStatement写好了以后呢,我们现在来开始设置问号对应的值,那就一个一个来,第一个问号对应的什么,是ps.setString(1, userInfo.getUsername());,就是第一问号,不就是这个的值么,是吧,然后同样,ps.setString(2, userInfo.getPassword());,第2个对象呢,是从userinfo里面去取的密码,是吧,然后,ps.setString(3, userInfo.getNickname());,第3个问号呢,是userinfo.getNickname(),然后,ps.setInt(4, userInfo.getAccount());,是吧,我不记得,当时我们那个有小数位么,account我不记得了,但是有小数位的话,你这应该用double,咱们那account应该在设置的时候,应该也是double类型,是这样的吧,如果要整数类型呢,那这块就写setInt了啊。那么这是userinfo.getAccount()
  • 那这样的这4个问号,是不是就都替换好了,然后呢,int d = ps.executeUpdate();,是吧,我执行这条sql语句啊,然后int d等于它,如果d>0,它就说明这条记录插进去了,是吧,插进去以后,这块实际上就什么呢,注册应该是成功了,就是我这条记录应该已经是插到表里去了,是吧,那咱们这就不写,如果跟业务逻辑没关系的话,我们这只是存数据的话,我们不写注册成功,就是什么啊,插入数据成功是吧,就相当于我们这条记录是保存到表里去了,然后呢,我们是不是就可以把对应的id值取出来,怎么取啊,ResultSet,是吧,它等于ps.getGenerateKeys(),是这样的吧,把这个字段的主键,把它返回回来,即ResultSet rs = ps.getGeneratedKeys();
  • 在这呢,生成主键,然后把那主键返回回来,然后,rs.next(),走一条,然后呢, int型的id,它就等于rs.getInt(1),第一个字段的值,是吧,然后把它设置回到userInfo上,setId(id);,是吧 ,因为它之前不就是没这id么,是吧,给它设置好,都完事以后,return true;,就表示我插入记录完毕,并且呢,你id也有了,你那userinfo就表示表里那条记录了,完整的都有了,是吧,都插进去啊,那如果不成功的话,最终出来以后,是不是再去最后return false;,是吧,那这样的话,这一个完整的方法我们就写好了,就是一个正常的保存数据了,是吧。
代码实现:
package dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import db.DBUtil;
import entities.UserInfo;

/**
 * DAO 数据连接对象
 * DAO是一个层次,该层里的所有类都是和数据库打交道得,作用是将数据操作的功能从也无逻辑层中分离出来,
 * 使得业务逻辑层更专注的处理业务操作,而对于数据的维护操作分离到DAO中。
 * 并且DAO于业务逻辑层之间是用JAVA中的对象来传递数据,这也使得有了DAO可以让业务逻辑层对数据的操作完全面向对象化。
 * @author soft01
 *
 */
public class UserInfoDAO {
	/**
	 * 保存给定的UserInfo对象所表示的用户信息
	 * @param userInfo
	 * @return
	 */
	public boolean save(UserInfo userInfo) {
		Connection conn = null;
		try {
			conn = DBUtil.getConnection();
			String sql = "INSERT INTO userinfo "
					     + "(id,username,password,nickname,account) "
						  + "VALUES "
					     + "(seq_userinfo_id.NEXTVAL,?,?,?,?)";
			PreparedStatement ps = conn.prepareStatement(sql,new String[] {"id"});
			ps.setString(1, userInfo.getUsername());
			ps.setString(2, userInfo.getPassword());
			ps.setString(3, userInfo.getNickname());
			ps.setInt(4, userInfo.getAccount());
			int d = ps.executeUpdate();
			if(d>0) {
				//插入数据成功
				ResultSet rs = ps.getGeneratedKeys();
				rs.next();
				int id = rs.getInt(1);
				userInfo.setId(id);
				return true;
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			DBUtil.closeConnection(conn);
		}
		return false;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

代码分析:UserinfoDAO重构RegServlet之保存

  • 然后,我们回到RegServlet来,那么咱们之前在这注册的时候啊,开始注册的时候,原来写的是不是就写文件里,对吧,现在其实我们不需要干那个活了,那开始注册以后呢,我们是不是就先获取用户输入的用户名啊,密码,昵称,然后实际上咱们这块,是不是还是应该先判断用户存不存在对吧,但是你们看啊,里面这一大堆,实际上是不是都不用干了,就是插数据库这都不用它干,当然这个后面就是成功以后怎么响应的问题,是吧,下面finally块,DBUtil关流也不用干了,都可以不要了,注释掉啊,判断是否插入数据成功,还是应该是有一个if的是吧,咱们就先写个true吧,这都无所谓了啊,先写个这个东西,然后它这块啊,forward是要抛异常的是吧,一会再说吧,先看上面啊。
  • 当我们拿到这个用户的这个注册信息以后,然后我们要做的事情是什么呀,保存用户信息,是吧,那我们想保存用户信息的前提,是我们干嘛呢,先这个创建一个UserInfo实例,用于表示该注册信息,那也就是说,用户不是给我们,username,password和nickname,这么几项么,对吧,那我们干嘛呢,我们就创建一个UserInfo,等于new一个UserInfo实例,然后呢,userInfo.setUsername(username);,然后呢,userInfo.setPassword(password);,是吧,然后再userInfo.setNickname(nickname);,然后呢,再userinfo.setAccount(5000);,也就是说,将来这个值是多少,是由业务逻辑决定的,就是说,数据库那边不管,你让我插什么数据,我就给你插入,明白吧,它不决定那个数据具体是啥,由你这决定好,这都是业务逻辑说的算啊,比如说,我这默认情况下,比如说,我们就认为啊,用户一注册,给你5000红利,是吧,就类似这个意思啊,在这块呢,我们这么写就行了。
  • 那这个信息都有了,其实实际上,就是没有id,对吧,因为id,我们确实是没有办法啊,就不由我们决定了啊,那些信息都有以后,我们怎么保存用户信息啊,首先我们创建一个UserInfoDAO,就new一个UserInfoDAO,有了它以后呢,我们是不是就调dao的什么方法啊,save方法,然后把它userInfo给进去,它会得到一个布尔值吧,是吧,我得给个标记啊,那么flag是不是就表示了它成功与否啊,你写success,就是你写什么都行,是吧,我们就写success,那如果success是true的话,那是不是我们就该跳成功页面了,否则的话,是不是应该跳错误页面去,是吧,那么咱们当然也可以给整个过程,捕获个异常,是这样的吧,或者如果它只需要forward这捕获的话,我们就在这块给它加个try-catch啊。
  • 那就是如果成功,就跳成功页面,这是不是就行了,是这样的吧,那如果失败的话呢,我们就跳到这个,注册失败的页面去,是吧,当然我这呢,没写失败是吧,首先输出一句话吧,在这注册失败啊,先打个桩啊,这样就行了吧,那也就由此可见,在这个RegServlet里面,你看这回,里面是不是就再也看不到操作数据库的影子了,我们只是先把用户的信息拿到,按理来讲,其实这个地方,还应该有什么啊,验证用户信息的正确,格式是否都正确,你里面该有不和谐的,那个什么啊,我去年买了个表啊,那些玩应,你该验证是不是都得验证好了,还记得那些事吧,你用正则表达式,该做的都得做了,明白么。
  • 那如果这些用户名,昵称等你都验证完了,还有那些啊,就是用户名是不是重的,是吧,用户名是不是重的,咱待会再说,那是不是都应该是你业务逻辑在注册的时候要干的活,对吧,但是真正你说你要想保存数据了,那不用你干,你把userinfo对象弄好,交给谁啊,dao,你给我存上,它就给你存上了,清楚吧,它就给你存好了,存好了以后,那我就看你存的结果对不对啊,如果你告诉我你都存上了,那我就给用户跳注册成功,如果没成功,我就给用户跳注册失败,能理解吧,那这样的话,我们等于说是不是就把业务逻辑和保存数据拆开了,是不是拆到了两个类里去干了,是这样的吧。
代码实现:
package service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import common.HttpContext;
import core.HttpServlet;
import dao.UserInfoDAO;
import db.DBUtil;
import entities.UserInfo;
import http.HttpRequest;
import http.HttpResponse;

/**
 * 用来完成用户注册功能
 * @author fhy
 *
 */
public class RegServlet extends HttpServlet {
	/*
	 * 我们用这个方法来完成业务处理
	 */
	public void service(HttpRequest request,HttpResponse response) {
		//此时重启服务器,客户端访问注册页面就可以在控制台显示“开始处理注册!”
		System.out.println("开始处理注册!");//打桩
		/*
		 * 将用户的注册信息按行写入到
		 * userinfo.txt文件中。
		 * 每行为一条用户的信息,格式:
		 * username,password,nickname
		 * 例如:
		 * fanchuanqi,123456,fanfan
		 * liucangsong,222333,cangcang
		 * 
		 * userinfo.txt放在webapp目录下
		 */
		//获取用户名,密码,昵称
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		String nickname = request.getParameter("nickname");
/*		
		Connection conn = null;
		try {
			conn = DBUtil.getConnection();
			//先判断该用户是否存在
			Boolean isNamed = comparedName(conn, username);
			if(isNamed||"nothing".equals(username)||"nothing".equals(password)||"nothing".equals(nickname)) {
				System.out.println("用户名已存在,或输入为空!");
				forward("/reg_info.html", response);
			} else {
				Statement state = conn.createStatement();
				String sql = "INSERT INTO userinfo "
							  + "(id,username,password,nickname,account) "
							  + "VALUES "
							  + "(seq_userinfo_id.NEXTVAL,'"+username+"','"+password+"','"+nickname+"',5000)";
				int d = state.executeUpdate(sql);
				if(d>0) {
					System.out.println("注册成功!");
*/					
		//创建一个UserInfo实例用于表示该注册信息
		UserInfo userInfo = new UserInfo();
		userInfo.setUsername(username);
		userInfo.setPassword(password);
		userInfo.setNickname(nickname);
		userInfo.setAccount(5000);
		//保存用户信息
		UserInfoDAO dao = new UserInfoDAO();
		boolean success =  dao.save(userInfo);
		try {
			if(success) {
					/*
					 * 响应注册成功的页面给客户端
					 */
					forward("/reg_ok.html", response);
				}else {
					//跳转注册失败页面
					System.out.println("数据库插入失败!");
					forward("/reg_info.html", response);
				}
		} catch(Exception e) {
			e.printStackTrace();
		}
/*			
			}	
		} catch (Exception e) {
			//如果有异常的话,将异常输出一下。
			e.printStackTrace();
		}finally {
			DBUtil.closeConnection(conn);
			//closeBrAndPw(br,pw);
		}
*/		
	}

	private void closeBrAndPw(BufferedReader br, PrintWriter pw) {
		if(br!=null) {
			try {
				br.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if(pw!=null) {
			pw.close();
		}
	}

	private Boolean comparedName(Connection conn, String username) throws SQLException, ClassNotFoundException {
			boolean flag = false;//用户名可用
			Statement state = conn.createStatement();
			String sql = "SELECT username from userinfo where username='"+username+"'";
			ResultSet rs = state.executeQuery(sql);
			if(rs.next()) {
				flag=true;//用户名已存在 
			}
			return flag;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127

  • 那咱们试一试,我们来启动一下服务,试一下效果啊,我执行,这块是localhost:8080/index.html,然后这是那主页是吧,然后去注册啊,比如输个用户名,密码123456,然后输入用户昵称,点击注册,哎,这个时候,大家可以看到,是不是仍然可以显示注册成功了,是这样的吧,那么,但是你们得知道,咱们这块,实际上拿到这些信息以后,我是不是都交给那dao去干的,它是不是就负责帮我把这个这几个数据,最终写成一个sql语句,但大家注意,你看,对于我们来讲,在RegServlet这个地方,如果我调用dao,我把数据给dao,实际上知道插到哪去了么,根本就不知道,因为保存的事,是不是交给dao去干了,我根本就不关注数据库那些表叫什么名,跟我没关系,我只管我的业务逻辑,对了,我就把数据交给你,你保存就好了。
  • 那也就是说dao是不是就是负责,知道是往那张表里存,甚至大家注意啊,你看啊,dao,虽然知道它是往哪张表里去存,但它知道这个userinfo表在哪个数据库,它连的是哪个数据库么,它都不知道,连哪个数据库,谁决定的啊,是DBUtil决定的,对不对啊,DBUtil不就负责建立这个连接么,也就是说建立连接是不是就又交给另一个类去干了,对吧,而DBUtil说我来负责帮你建立连接,那DBUtil实际上知道连接是哪么,也不知道,它只是说什么啊,交给线程池了,线程池来负责建立连接,而线程池知道在哪建立连接么,也不知道,听配置文件的,是吧,我配置文件说连哪,你就连哪,你就负责连接,DBUtil负责连接好以后,把连接交给dao,是吧,dao负责把它插到表里去,是吧,Servlet就负责处理它的业务逻辑交给你处理。
  • 也就是说每一层是不是都给它分开,对吧,大家各干各的活,咱们都细碎一点,其实也就由此可见,咱们整个数据持久层有dao,有DBUtil,是吧,它们是不是都是属于数据持久层的工作,连数据库的相关的工作,是吧,都是交给它们去干了,所以就是说,我们程序现在,虽然切的比较碎了,但是将来我们改哪一部分,其他类是不是都不用动,比如说我现在想换个数据库,动哪啊,动配置文件就完了,什么都不用动,对不对,你想换个库,是不是都没问题,你换成MySQL,只要MySQL也有这些表,一样是不是能插进去,你的程序是不用动的,明白么。
  • 那比如说吧,将来说我不想存数据库里,我就想写文件里,怎么办啊,那你是不是只改UserInfoDAO类就行了,它就拿到数据以后,别插往表里去了,它就直接传给一个文件,写文件里就好了,但是对于我们Servlet而言,它这有变动么,它这也没有,它也不需要管这事,反正我就把数据交给你,你爱写哪写哪,那是你的问题,你想存数据库,存库里,你想存文件,你存文件里,对吧,但是对于我业务逻辑来讲,我不关心这个事,是这样的吧,那也就说,我们现在就把这个程序给它分的很多层了啊。

4.小总结:WebServer执行流程

  • 我们再缕一缕WebServer这整个思路啊,怎么一个过程啊,首先呢,我们先来到的那个是reg.html页面,是吧,咱们先打开的是这个页面啊,然后呢,这个页面一提交,这个form表单提交,那么这个form表单提交的话,是不是就提交到那个,叫作/reg那个位置啊,那也就是说那个/reg,会被我们的WebServer整到哪去,会整到这个RegServlet来,是这样的吧,那也就是最终,这个过程知道怎么来的吧,咱们之前写的,在那个ClientHandler,我们处理那uri什么的,是吧,只要最终就是,总之就是拿到了这个表单中的数据,传到了RegServlet来,那我们在这干嘛呢,处理什么呢,处理业务,就说它这块是负责什么呢,处理业务的,是吧,这个地方是什么呢,就是用户提交注册信息,是吧,用户提交的这个注册信息,然后通过form表单提交到我们的RegServlet来,这Servlet呢,就负责处理业务,那它是不是就可以把,从用户那里拿到的那些信息,我们通过Request,获取那些用户名啊,密码啊,是吧。
  • 然后,原来我们是不是,自己就在RegServlet这个地方,直接就连数据库,这就insert,就插进去了,是吧,那我们说了,现在咱们不这样做了,我们这块干嘛呢,又中间追加了一层,追加了一层是谁啊,就是那个UserInfoDAO,是这样吧,也就是说,我们这块呢,就把那数据交给了它,那怎么交给它啊,这个过程中实际上,我们是不是把一个对象进行了传递,因为我们把那个弄好的数据放到了一个什么呢,叫作UserInfo,这么一个对象,是这样的吧,那也就是说,当我们把用户拿到的这些信息啊,我都处理完了,得到这用户名什么的这些,登录验证,之前没写,但实际上应该都会有,是吧,而且验证都没错的话,它就把这对象交给了UserInfoDAO,跟它说什么,你把这个对象给我存上,是吧。
  • 然后,DAO负责干嘛呢,dao负责往这个数据库里去存,对吧,而它(dao)要往数据库里面去存的话,实际上它这块中间,还有一个谁啊,就是还有一个DBUtil吧,是这样的吧,也就是说,它(dao)从这个地方DBUtil这块,获取了什么啊,连接,对么,也就是说,它这个(数据)是form表单提交给RegServlet,这是第一步;RegServlet将员工信息对象传给UserinfoDAO,这是第二步;UserInfoDAO通过DBUtil获取连接,这个是第3步;那当我们通过这个DBUtil获取连接以后,那第4步呢,那UserInfoDAO是不是就等于,将员工对象UserInfo往数据库里面进行insert操作吧,将员工对象存入到数据库,那这就是我们说的第4步,是这样的吧。
  • 所以也就是说,相当于用户,你们这一提交注册,然后后面要做这么多件的事啊,就首先先把信息提交到服务端来,RegServlet在这正常的业务处理的话,应该是至少得先把你的用户名,密码做一些必要的判断,包括我们实际上,这块还少了一个工作,就是至少你得先查一个用户存不存在,是吧,这个活咱没干呢啊(没有通过Dao干),但总之咱现在就直接了当的,那也就是说啊,如果数据已经被我提交过来以后,它是不是就相当于在这个Servlet里面,我们就把这个用户提交完的那些数据,组建成了一个UserInfo的对象,然后我们把这个对象传递给dao,是吧。
  • 传递给dao以后,dao干嘛呢,它负责从那个DBUtil那,这个地方获取个连接,然后想办法,把它变成一条insert语句,卡,插表里去了,插到表里以后呢,回馈给Servlet,true,成功,然后Servlet呢,是根据这个OK,响应客户端注册页面,那个,就那个注册成功的页面,我们就看到了一个注册已成功,如果它这块,在这个过程中,注册要是失败了话呢,是不是它就负责,返回失败,就跳到错误,跳到错误页面去,是这样的吧,所以就是说,整个这个过程,我们就分开来这么几步,可以理解么,也就是说,咱们曾经写的时候,是不是就只是没有UserInfoDAO,咱们那时Servlet,直接就通过DBUtil,获取连接,insert插表里去了。
  • 现在我们又怎么样呢,把这个操作数据库的事,单独拎出来,又交给了一个类,那这样的话,是不是就相当于,我们如果分层次看的话,就相当于RegServlet这个地方是处于业务逻辑层,UserInfoDAO和DBUtil与数据库是属于数据持久层的工作,明白吧,我们给它分开了,分成这么两个层次,当然将来,我们后面还会,再会再做一个细分, 还会有一个控制层,也就是说我们把这个,这个啊,业务逻辑和这个数据持久层的一大堆东西,把它们都归定到模型层啊,那个等后面用MVC什么的事了,总之,我们现在先,先做成这个样子啊。
  • 那么,所以呢,在这个UserInfoDAO里呢,先定义个save方法,在这个save方法这里呢,我们把那个数据插进去啊,然后呢,我们用Servlet去调一下,然后呢,大家注意,那我们就可以在RegServlet,这个注册这边呢,进行一些修改了啊,那我们注册这里面怎么改呢,在这RegServlet里面,咱们原来获取的用户信息以后,是不是就直接连数据库,插入,插入进去了,是吧,现在我们把它换成什么呢,换成那个啊,如果之前有那个已经测了的,那个用户是否存在,可以先把这些都注掉,就把原来,直接操作数据库那点东西都先注掉,明白吧,可以先保留啊,然后我们先做成这种操作,就是拿到用户名,密码,昵称这些信息,这不能上来就写注册成功是吧,而是先干嘛呢,先把它变成一个java中的对象,是吧,要存的数据存进一个对象,然后呢,创建一个DAO啊,我们叫到,然后调它的save方法,是不是就存上了,对吧,存上了以后呢,返回一个结果,如果它是true,我们是不是就跳成功的页面,else,是不是它就应该跳失败的页面,是这样的吧。

5.重构WebServer项目之数据持久层二

代码分析:UserInfoDAO.findByUsername(…)

  • 然后,RegServlet这个地方,我们实际上,在这个注册之前,创建一个UserInfo实例用于表示该注册信息,这之前,首先要先做一个事,是什么呢,首先验证该用户是否已经存在,是这样的吧,那么这个时候怎么验证呢,是不是就根据用户给的用户名,是不是就可以来进行判定了,对吧,那我们怎么进行判定呢,那么简单说,我们是不是可以用这个用户名,去表里查查有没有这个人,如果有,是不是就说明,这用户已经存在了,对么,所以,大家注意啊,那我们就可以在这个UserInfoDAO当中呢,我们再来加一个方法,就是查用户信息的啊,当然这个法方法,你们将来,可能还会用在别的地方。
  • 比如说我将来查好友,也可以用到这个方法,你们见那网站不是,正常有那种操作,比如说我可以查某一个用户,查某一个好友,根据名字,是不是就能找人,微博里面,也经常有这个功能么,是不是啊,那所以就是说,这个方法,你在这个地方,可以把它,找着这个人了的这个功能,那我们用途可能很多,你如果在那注册,那找着这个,说明你不能再注册,这个用户已经存在了,如果要是找人,我是不是也同样可以用到这个方法,对吧,所以将来这个方法,还是比较通用的啊,就是在这块呢,我们定义这么一个方法,是什么呢,比如叫作,public UserInfo findByUsername(String username) {},就是根据用户名找人,然后如果找到的话,会返回一个UserInfo的实例。
  • 说一下这个findByUsername方法啊,这个方法呢,咱们就可以根据用户名呢,去查这个员工信息,如果你查到,你就把这个员工信息呢,变成一个UserInfo的实例,怎么做啊,语句怎么写啊,类似于就是,首先,SELECT * FROM userinfo,当然这不应该写*号啊,我就这意思了啊,然后呢,WHERE 传的什么呢,username='',即 SELECT * FROM userinfo WHERE username='';,我说一点啊,我是这么写的,我只是告诉你,这sql语句,应该是用这个,WHERE条件是username,用这个判断,因为你是不是根据用户名找么,别我这么写,你也这么写,username='"++"',里面就开始拼字符串了啊,这块应该是什么呢,问号还得是问号啊,这应该username=?,还得是问号啊,这还是得有动态信息的啊,然后这个地方不要写*号啊,我这只是示意啊。具体sql如下:

String sql = "SELECT id,username,password,nickname,account "
			  + "FROM userinfo "
			  + "WHERE username=?";
  • 1
  • 2
  • 3

  • 那这样的话,是不是就可以把这个用户查出来了,如果这个结果集,next没有记录,是不是说明没人,那就return null;,如果有人的话,就是不是,应该把各个字段的值都得到,然后是不是保存到一个UserInfo实例上,把这个UserInfo实例返回就好了,能理解我的意思吧,一会咱们可以用,在我们这个,作为这个检查该用户是否存在啊,大家一定要注意一个原则,就是什么呢,就是DAO啊,就是这个到,它和业务逻辑,将来打交道,都是靠对象,互相传对象啊,就是你给我用户的信息,也是以用户对象给我,我返回给你,你管我要,我也是返回给一个什么呀,UserInfo的实例啊,表示这个员工的信息,也就是说,业务逻辑层,它其实看不到数据库的影啊,就是你不需要管,反正你数据给我,我就想干嘛啊,说到底咱们俩之间交流,就是对象啊。
  • 然后再说一点啊,还是要有一个习惯,就比如说你这方法啊,其实就跟咱们之前那个UserInfoDAO中的sava方法似的,写好了的话,非得和这个Servlet配合才能用么,也不是吧,怎么做啊,可以在这个类中写个main方法,是不是就可以啊,比如说,UserInfoDAO dao = new UserInfoDAO();,自己是不是先创建个DAO,就new一个UserInfoDAO,然后是不是可以自己组建个对象,调一下那个方法findByUsername,先看你这自己这块成不成功,如果成功的话,你将来Servlet是不是一调,也应该也是可以的,明白么,将来做测试的时候,都是这样啊。
  • 比如说,将来工作就会有这种情况,比如说你的同事写Servlet,你负责写dao,那你不能说,你写完了你等着人家,你等人家写好了之后再测,那不可能的,你就测你自己的,你保证你自己数据没问题就完了,是吧,数据你先自己提供一个假的,你先看看能不能插入,能,没问题,那等人家配上的时候,它要是给你插入不成功,那是他的事,那不是你的问题,清楚么,都这么测,那个findByUsername,是不是也可以测啊,然后就调什么啊,dao.findByUsername("jack");,你这比如自己写一个,然后看看,得到那个UserInfo实例,对不对么,看看它输出的是啥,是不是都可以自己先打个桩,先测测,明白么,都这么干啊,先保证你这个,这个方法没错,然后一会咱们再配合,用Servlet调一下就好了啊,一会再写哪个方法的时候,都是先这么测啊。
  • 那写一写这个方法啊,那这个findByUsername怎么做,首先,这个username信息已经给你,你就直接连上,查就行了,是吧,然后Connection conn = null;,然后try-catch-finally块,把基础的工作先做好啊,DBUtil关闭连接,建立连接,如果最后报错的话,然后我们就怎么样,我们就返回null了啊,然后在try里面建立好连接之后呢,我们先写条sql语句,这条sql应该长什么样子啊,SELECT id,username,password,nickname,account FROM userinfo WHERE username=?,是这样的吧,然后呢,创建PreparedStatement,是吧,那就等于什么啊,Connection的方法,就是PreparedStatement(sql);,然后呢,我们可以设置问号是吧,ps.setString(1, username);
  • 然后呢,我们是不是就可以进行相应的查询工作了,ResultSet rs = ps.executeQuery();,ps执行executeQuery,是吧,得到ResultSet,得到结果集以后呢,如果,rs.next()找到数据了,那我们就把那些信息都给它取出来,是吧,比如int型的id,等于rs.getInt(“id”);,然后,username = rs.getString("username");,username因为上面参数已经定义过了,是吧,所以在这,我们就直接用就行了啊,然后,String password = rs.getString("password");,然后是,String nickname = rs.getString("nickname");,然后呢,int account = rs.getInt("account");,这几项都有了以后,我们是不是就可以实例化这个UserInfo表示它了,userinfo,它等于new一个UserInfo,是吧,然后把这几个参数,传进去,是不是就可以了,对吧,传进去以后呢,我们就return这个userinfo,就完事了吧,是这样的吧。
  • 就是正常的把它(要查询的用户)从数据库里查出来,把这各个字段值都拿到以后,把它变成一个UserInfo的实例,然后返回,也就是说,给那个,将来我们要给业务逻辑层的时候,是给它一个UserInfo的实例,是吧,你甭管我从哪来的,当然我们这块可能是从表里查的,是吧,但总之,我们对外的时候,就告诉你,这就是你要查的那用户的信息了,我们以对象的形式给它了。
代码实现:
public UserInfo findByUsername(String username) {
	/*
	 * 根据给定的用户名查询该用户信息,若没有记录则直接返回NULL,若查询到,将该条记录各个字段的值取出来存入到一个UserInfo实例中并返回。
	 */
	
	Connection conn = null;
	try {
		conn = DBUtil.getConnection();
		String sql = "SELECT id,username,password,nickname,account "
					  + "FROM userinfo "
					  + "WHERE username=?";
		PreparedStatement ps = conn.prepareStatement(sql);
		ps.setString(1, username);
		ResultSet rs = ps.executeQuery();
		if(rs.next()) {
			int id = rs.getInt("id");
			/*
			 * username因为上面参数ps.setString(1, username);
			 * 已经定义过了,所以在这我们直接用就行了
			 */
			username = rs.getString("username");
			String password = rs.getString("password");
			String nickname = rs.getString("nickname");
			int account = rs.getInt("account");
			UserInfo userInfo = new UserInfo(id, username, password, nickname, account);
			return userInfo;
		}
		
	} catch (Exception e) {
		e.printStackTrace();
	}finally {
		DBUtil.closeConnection(conn);
	}
	return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

代码分析:UserinfoDAO重构RegServlet之验证

  • 那么这个findByUsername方法写好以后呢,咱们怎么用它,在RegServlet注册这边,咱们进到service里面来以后,那这个时候,我们看这块,如果拿到这些用户名密码,昵称,以后,怎么先判断啊,这个时候,咱是不是可以把这个,UserInfoDAO dao = new UserInfoDAO();,这件事放验证该用户是否已经存在的上面,也就是说,我们上来就先这个,实例化好这个dao啊,然后呢,在这判断的时候,怎么做呢,首先,我们调用这个dao的findByUsername(username),然后呢,是不是会得到一个UserInfo的实例,是这样的吧,然后,我们可以直接if里判断,看它得到的值,是不是不等于null的,如果不等于null,说明什么啊,就有这人,是吧,有这人的话呢,那就什么呢,该用户已存在,否则呢,再做咱们之前,下面那个注册的那一大堆事,是这样的吧。
  • 所以把它们就放到了一个else里面,是不是就行了,if-else里面,对吧,就把原来这个啊,注册的这些信息,把它全放到else当中啊,就是如果有这个人的话,就先干该用户已存在这件事,如果没有的话,我们再把它们都这个进行,设置到UserInfo实例,然后,进行save啊,那么,当然在这个地方,我们还可以先做个,做个页面出来吧,比如说,这是一个注册的提示的这一个页面,reg_info.html啊,说什么呢,该用户已存在,是吧,比如这块可以别写失败啊,其实呢,在将来,你们在写程序的时候,也会有这个问题啊,就是一般,我们尽可能少用什么啊,失败呀,错误呀,这种,这种词语,就会给用户一种很不爽的这种感觉,就会潜移默化的,一般就提示那个,就对不起,您那个用户已经存在了,你也不能够告诉它,注册失败,是吧,这个什么,这个就有点生硬,哈哈,恩。
  • 然后,比如说,但实际上确实是失败了,是吧,然后,那这个地方呢,我们就跳转到哪个页面去了,forward("/reg_info.html",response);,就跳这个页面就行了,然后呢, 试试啊,这个啊,我们就用这个来试试啊,就是fanchuanqi,密码123456,然后再这个,是吧,如果,咱们还是用这个帐号,一执行,这样的话,它就告诉你,该用户已存在了,是吧,如果没问题的话,我们可以,比如说再写一个吧,这样的话,就正常的话,是不是就可以注册成功,是这样的吧。
代码实现:
package service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import common.HttpContext;
import core.HttpServlet;
import dao.UserInfoDAO;
import db.DBUtil;
import entities.UserInfo;
import http.HttpRequest;
import http.HttpResponse;

/**
 * 用来完成用户注册功能
 * @author fhy
 *
 */
public class RegServlet extends HttpServlet {
	/*
	 * 我们用这个方法来完成业务处理
	 */
	public void service(HttpRequest request,HttpResponse response) {
		//此时重启服务器,客户端访问注册页面就可以在控制台显示“开始处理注册!”
		System.out.println("开始处理注册!");//打桩
		/*
		 * 将用户的注册信息按行写入到
		 * userinfo.txt文件中。
		 * 每行为一条用户的信息,格式:
		 * username,password,nickname
		 * 例如:
		 * fanchuanqi,123456,fanfan
		 * liucangsong,222333,cangcang
		 * 
		 * userinfo.txt放在webapp目录下
		 */
		//获取用户名,密码,昵称
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		String nickname = request.getParameter("nickname");
/*		
		Connection conn = null;
		try {
			conn = DBUtil.getConnection();
			//先判断该用户是否存在
			Boolean isNamed = comparedName(conn, username);
			if(isNamed||"nothing".equals(username)||"nothing".equals(password)||"nothing".equals(nickname)) {
				System.out.println("用户名已存在,或输入为空!");
				forward("/reg_info.html", response);
			} else {
				Statement state = conn.createStatement();
				String sql = "INSERT INTO userinfo "
							  + "(id,username,password,nickname,account) "
							  + "VALUES "
							  + "(seq_userinfo_id.NEXTVAL,'"+username+"','"+password+"','"+nickname+"',5000)";
				int d = state.executeUpdate(sql);
				if(d>0) {
					System.out.println("注册成功!");
*/					

		UserInfoDAO dao = new UserInfoDAO();
		//先验证该用户是否已经存在?
		if(dao.findByUsername(username)!=null||"nothing".equals(username)||"nothing".equals(password)||"nothing".equals(nickname)) {
			//该用户已存在!
			try {
				forward("/reg_info.html", response);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}else {
			//创建一个UserInfo实例用于表示该注册信息
			UserInfo userInfo = new UserInfo();
			userInfo.setUsername(username);
			userInfo.setPassword(password);
			userInfo.setNickname(nickname);
			userInfo.setAccount(5000);

			//保存用户信息		
			boolean success =  dao.save(userInfo);
			try {
				if(success) {
						/*
						 * 响应注册成功的页面给客户端
						 */
						forward("/reg_ok.html", response);
					}else {
						//跳转注册失败页面
						System.out.println("数据库插入失败!");
						forward("/reg_info.html", response);
					}
			} catch(Exception e) {
				e.printStackTrace();
			}
		}
/*			
			}	
		} catch (Exception e) {
			//如果有异常的话,将异常输出一下。
			e.printStackTrace();
		}finally {
			DBUtil.closeConnection(conn);
			//closeBrAndPw(br,pw);
		}
*/		
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115

6.重构WebServer项目之数据持久层三

重构登录功能简述

  • 然后,登录我们是不是可以再加一个方法,在UserInfoDAO当中,是不是可以再加一个,是吧,加一个什么啊,比如你,在这块再给它,再多出一个新的方法,因为这个登录的话,是不是就是靠用户名密码去查用户了,是吧,你这可以要求,比如说,public UserInfo findByUsernameAndPassword(String username, String password) {},同样如果要是能查着的话,是不是就把UserInfo对象给你,没有的话就返回null,明白吧,那就是说,你这输入用户名,密码对不对啊,只要有一个不对,那我也给你返回成null,总之就是有用户就传,没有用户就说明什么啊,这是根据给定的用户名和密码查询该用户。
  • 或者其实大家注意啊,其实用下面这个findByUsername的这个方法,其实也无妨,就干嘛啊,先把这个用户查出来,然后比比密码就行了,能理解我的意思么,它不是用户名密码了么,你先用户名,是不是用这个findByUsername,把这人查出来,因为我这查出来的话,我这UserInfo里面是不是有这个人的密码,然后把你输入的密码,再跟这比一比,看看一不一样,一样,那就登录成功,不一样就登录失败,是不是也可以,是吧,OK啊,不过,这块再练一个吧,那就单独再这个写这么一个方法啊。
  • 然后呢,我们来到LoginServlet,在LoginServlet当中,原来咱们拿到用户名密码,是不是还是直接连到数据库,现在是不是不用这么干了,我们干嘛啊,还是创建好DAO,然后findByUsernameAndPassword,把这个人拿到,如果有就登入成功,没有,就登录失败,是吧,如果你要是用之前咱们写的那个findByUsername,就是咱单独拿那个,是不是拿到这个对象以后,还得判断一下密码,是这样的吧,这两种都可以,不过咱在这块呢,先使用我说的这种,就是再单独先加一个findByUsernameAndPassword方法啊。
  • 实际上大家注意啊,DAO当中,咱们现在是不是其实只写了两种,一个是save方法,就是用来保存数据的,还有一个就是查询的那两个方法吧,其实真正我们的查询方法,还有好多个,比如有findAll,findAll是什么啊,查所有用户,因为你们实际上在这个,比如你们去某些论坛,是不是也经常看到,比如我想看看都有哪些用户,就会把所有用户都列出来,是吧,就是当然,你不会列所有用户的全部信息了,比如你只列它的名字啊,还有什么这个昵称,你可以让人看的这些东西,是吧,至少我们有查询所有用户这种请求,这种操作,还会有什么啊,删除修改是不是都会有,比如说delete呀,update呀,就比如你们之前在网上,是不是也有修改个人信息的,我改我自己的什么昵称,我改改密码,对不对,那像这个update的操作也都会有啊,所以实际上咱们将来,咱们这个DAO针对的业务逻辑,都会给它提供相应的方法,明白吧,就是什么增删改查的都会有啊。

增添密码修改功能简述

  • 那再做一个改用户信息,改用户信息怎么改啊,但是咱们正常情况下,改用户信息的前提是,你得是个登录用户,是吧,我一改的话,就相当于把注册页面的标题改为修改用户,然后还是注册的那三项信息啊,再多一个吧,比如多一个金额,可以有这么4个值啊,金额先改一改吧,咱现在不太在意那个具体业务,因为实际上我们有好多东西都干不了呢,还啊,正常情况下你要改的话,就只能改自己,没有人说你登录进去,你让别人也能改,那是不可能的,是吧,但先不考虑那些东西了啊,你就至少你能做到一点,比如说我,我改这个人,我把它的密码改成123465,你就输这个密码就行了,然后昵称,你给输入一个,下面有改金额的话,金额也输入一下,一点击修改,然后这个人信息就变了,能理解吧,就变这个人自己的信息。
  • 还有一般情况下啊,用户名是不允许改的,就是你可以输这个用户的名字,但是名字这个是不能改的,用户名,你看你上哪个网站,没有让你改用户名的,只能改昵称,只能改密码,你的用户名肯定改不了,是吧,所以咱们都让他输这个用户啊,就是输完了以后,我如果这个用户存在,我就给它修改,不存在,你就给它提示,你说,该用户不存在,明白吧,密码你可以改啊,那正常上网的话,密码应该输两次,是吧,那我告诉你,现在还做不到,因为实际上咱们还要做到什么啊,你还得有JS,你还得能做验证啊,就是这页面啊,你还得做一些脚本验证,是吧,在这块能干的先干了啊。
  • 比如说,我这输完名字,输完这个,给它输个新密码,输个新的昵称,然后一点击修改,这注册俩字,你那button那不是自己设置的那个么,那你就把它那值改成修改就行了是吧,你这一修改,就提交到后面,我们再做一个Servlet,是吧,就是叫作,比如叫作UpdateServlet,或者叫作什么无所谓,然后你就在DAO里面提供这么一个方法,是吧。

登录功能代码实现简述

  • 那先把这个功能写一写,UserInfoDAO的findByUsernameAndPassword的这个方法,根据这个用户名,密码,查这个用户信息,这前面这个都一样啊,try-catch-finally块,DBUtil关闭连接,建立连接等,然后呢,在try里面,我们怎么做啊,先写个sql语句是吧,

String sql = "SELECT id,username,password,nickname,account "
			  + "FROM userinfo "
			  + "WHERE username=? AND password=?";
  • 1
  • 2
  • 3

  • 首先,SELECT id,username,password,nickname,sccount,然后呢,FROM userinfo ,然后,WHERE username=? AND password=?,是吧,然后呢,创建PreparedStatement,是吧,它等于,conn连接点prepareStatement(sql),把sql语句传进去,然后呢,ps.setString(1,username),第1个就是username,然后setString,第2个就是password,是这样的吧,然后呢,执行executeQuery,返回ResultSet,然后呢,if(rs.next()){},有,就是findByUsername方法中的if(rs.next()){}里的那段,是吧,那我们就这个啊,得到这个用户名密码,什么这一大堆,是不是就生成一个UserInfo实例,传回去就行了。
  • 那如果比如说,要报错的话,或者最后报错的话,我们就统一的,return null;,就完了是吧,然后在登录这面怎么干啊,在LoginServlet中,那么,看你那个用户名密码拿到了以后呢,原来咱们是不是就得连数据库了,是吧,现在这些事不用干,是吧,那咱们怎么做,咱们可以比如说,拿到以后呢,创建这个UserInfoDAO是吧,new一个UserInfoDAO(),然后呢,它要它的findByUsernameAndPassword(username,password),是吧,这样的话,是不是就可以得到了这个UserInfo的实例是吧,同样的,那个如果if(userinfo!=null){},就说明服务器有这个人的信息了,登录成功,实际上将来,你们还会把它,放到这个会话里,就是这个session里面,作为这个用户的登录凭证啊,那个等后面用到这个,实际咱们使用Tomcat的时候再说了啊。
  • 那么,总之这样的话,我们是不是就可以,完成它的登录操作了,是吧,这个后面finally块DBUtil关闭连接,没有这个东西了是吧,那这就是,咱们说的那登录操作了,然后呢,咱们再来做一个修改,修改的话呢,咱们先搞个页面出来,咱们就参照我们的那个注册页面啊,reg.html是吧,就把那个页面,我们复制一份,我们再给它粘过来啊,粘一个,比如咱们叫作,update.html,复制你那个注册页面,那里不是信息多一点么,然后呢,这个地方我们就写成修改用户,h1,我们说叫什么呢,用户修改,然后,这个form表单呢,咱们提交的时候,是不是这个action,这个得变一下,是不是咱们换成update啊,然后呢,这有用户名,密码,昵称,然后呢,咱再加一项啊,就是那个余额是吧,name="account",然后底下这块呢,我们提交按钮呢,我们写成修改啊,如果用JS的话,你们才会知道,这修改咱不能说点一下,啪,就提交过去了,点一下一般会怎么样,提示一个,确定修改么,是,再提交过去,那将来再做啊。
  • 好了以后咱先看看这个页面啊,对不对啊,访问这个localhost:8080/update.html,就进到这个页面,这现在要真的点修改的话,它是不是给你跳到这个,没有这个页面来,因为我们是不是要到update这块来了,是吧,我现在没有update这个响应提示的页面啊,而且大家注意,你这个地方是不是正常情况下,你看,咱们这块控制台,也出了这么一个问题啊,之前就有同学就遇到这个事了,就是你们现在如果直接点击,这块有没有见到,这个ArrayIndexOutOfBoundsException,告诉你什么呀,数组下标越界了,是吧,原因在于什么呢,你看,咱们如果直接提交的话,就相当于你这输入框里什么都没输,是这样的吧,什么都没有输入的话,大家注意。

处理请求信息不完整问题

  • 那咱们到时在解析这个get请求的数据啊,你看咱们在解析,在HttpRequest类中,就是按照等号拆分,就是咱们之前的每一个,这一项,它用户名等于一个值,密码等于一个值,咱们是不是都是按等号拆的,而这个时候,你要按等号拆完以后,我们就只有一项了,只有左面那项,后面没东西,是吧,就是因为咱没输密码啊,那这个实际上,我们要改正的话,怎么办啊,是不是还可以在这做个判断,就是在HttpRequest这个类中,遍历每一个参数,按等号拆完了以后,这个地方,我们应该怎么做啊,按照等号拆完了以后,这块你看啊,咱们之前是不是首先,先按照什么&进行拆分,按照&拆完了以后,这个,这个数组当中的每一个元素,你看啊,就相当于什么,username=&password=&nickname=&account= ,按照&拆完了以后,那就是相当于,拆出来每一个是username=password=
  • 我就以username=,这一个为例了,那它现在是不是长这样,那你想想,我们要是按照等号,拆开的话,是不是就相当于左面的拿到,右边什么都没有,对吧,所以就是说,说白了,你这数组只有一项,大家注意,你们记不记得我跟你们说过,如果咱们这按照逗号拆,比如我连续有两个逗号,中间是不是会拆一个空字符串,我记得我跟你们说过这个事,我说惟独这个连续的逗号要放到末尾,拆完这些空字符串,我后面的逗号就都不要了,有说过这么个事吧,所以就相当于你这个要是末尾出现等号呢,也就是后面本身应该能拆出一个空字符串,但那个就不要了,那一项,那等于就只剩下一项了,对吧。
  • 所以就是说,咱们原来你看,咱们拿到以后,一定是,this.parameters.put(paraData[0], paraData[1]);,这块写死的,就是两个的,左边的和右边的,另一项paraData[1]是不是不一定有,对不对,那在这我们应该怎么办啊,至少是不是应该先做一个判断,首先判断什么呢,你拆出来的这个数组,它的length首先是不是得大于0,if(paraData.length>0) {},大于0,是不是说明至少你是拆出东西来了,对么,拆出东西来以后呢,那是不是还有两种情况,第一种情况,是你拆出的到底是一项,还是两项,对吧,所以在if(paraData.length>0) {}里面呢,我们可以再进行if判断,if(paraData.length==1) {},就是什么呢,看看它的length,是等于1的么,如果等于1的话,说明什么,就相当于是不是就是username=这个样子的,只有左边,没有右边。
  • 那这种情况的话,怎么办呢,我们往这个Map里面放的时候,我们这后面比如说放一个null,this.parameters.put(paraData[0], null);,明白吧(这里实际代码里放了nothing,this.parameters.put(paraData[0], "nothing");,用于在后面业务中作为标识判空),就放个null进去,那么,否则的话呢,否则的话,是不是就应就这两项了,else,否则的话呢,我们就把0和1,都给它放进去,这个是不是没有问题,所以,大家可以按照我这个,咱把这个拆分呢,稍微改正一下啊。

密码修改功能代码实现简述

  • 那么,这个改好以后呢,咱们来做一个,这个修改这个操作啊,这个修改这个怎么干啊,首先咱们是不是得先加一项,在这个service包里面,我们再加一个Servlet出来啊,加一个这个叫作UpdateServlet这个类,然后呢,它干嘛,跟他们一样,是不是它们也得继承,这个HttpServlet,跟RegServlet,LoginServlet放一起啊,这个Servlet加上以后,我们还要改什么地方,ClientHandler,是吧,因为在ClientHandler当中,我们是不是还没有针对这个修改,那个请求,是吧,所以就是说,咱们把那ClientHandler类里,之前在查看请求一个功能下面,咱们加过/reg的,加过这个/login的么,是吧,也就是说,咱们在这,还要再补一个,补一个什么呢,之前咱们这个udate.html页面的,这个请求的form表单访问的是update,是吧。
  • 所以就是说,我们在这个ClientHandler当中,还应该有一个/update吧,是吧 ,如果是update的话,我们这块访问的是那个UpdateServlet啊,加这个就行啊,那么这个写好了以后呢,那咱们在这个UpdateServlet里面,是不是就可以开始,比如你通过request,是不是可以先获取用户输入的用户名啊,密码啊,昵称啊,account那几个值,是吧,但是唯一需要注意的是,account,这个拿回来的可是个字符串,就是咱们这块页面上,你这个页面上,就算你输入的是数字,你这拿到的也是字符串,你是不是要先给它转成int值,是这样的吧,然后,变成一个UserInfo的实例,是不是调我们的这个DAO的update的方法,就把这个对象给进去,然后呢,这个DAO呢,就按照你这个对象里面,新的样子,给它进行修改啊。
  • 所以就是说,这个都写好了以后呢,我们现在在dao当中呢,我们再提供一段新方法啊,在UserInfoDAO当中,就是它也提供了一个修改用户信息的方法啊,public boolean update(UserInfo userInfo) {},就是说,将来这个业务层,把这个UserInfo给我们以后,这个里面是不是就记录了用户想改的内容,然后,前提是它的名字是不能改的,所以在这不改名字,把这UserInfo里面,设置的密码啊,昵称啊,还有account,是不是可以换成你这个的值啊,然后条件就是按照用户名改就行了啊,如果改完你发现影响表中,一条记录,那就修改成功,return true,没改成功,return false;,就完事了啊。
  • 那我们这个UpdateServlet里面怎么做啊,这块进来以后,首先先获取用户输入的,用户名等信息,是吧,都拿到了,因为这个实际上我是不是把那用户信息都提交过来了,那咱们是不是通过,request.getParameters,把修改信息的那几个用户名,密码等的,那几个值都拿到了,是吧,那都拿到以后,然后呢,这是第一步,先获取用户信息,那第2步呢,干嘛呢,是不是你得先看看,他是不是这个用户名,有没有这个人,是吧,所以这个干嘛呢,第二步干嘛啊,先判断该用户是否存在,是吧,不存在,则提示用户,就是你跳一个页面啊,没有该用户的提示页面。
  • 比如说第一步,拿到用户那些用户名密码什么的,都给它提交完东西是吧,我先看看用户输入的那个用户名存不存在,如果不存在的话,就直接跳到页面,就这个人不存在,就跟注册一样,是吧,注册的时候,就是提示页面就是已存在,有这个人,你这块你要是想修改的话,前提是得有,是吧,所以你要输的没有的话,我再给你提示,是吧,你这人不存在啊,如果要是有这个用户的话,我们干嘛呢,就是将用户输入的信息,这个存入一个UserInfo实例中,是吧,而第4步呢,调用UserInfoDAO的,哪个方法啊,update,是吧,进行修改,第五步,若修改成功,跳转修改成功的提示页面是吧,否则跳转失败的提示页面,就完了吧。
  • 所以它其实和我们注册的功能,其实就是差不太多,是吧,那就按照,可以按照类似于注册的那个流程啊,可以把这个完整的走一趟,其实就俩类了,一个就是update方法,DAO里的是吧,还有一个就是在这块,UpdateServlet中的service方法,完整的再写一个啊。

代码实现:UserinfoDAO.findByUsernameAndPassword(…)

/**
 * 根据给定的用户名和密码查询该用户
 * @param username
 * @param password
 * @return
 */
public UserInfo findByUsernameAndPassword(String username, String password) {
	Connection conn = null;
	try {
		conn = DBUtil.getConnection();
		String sql = "SELECT id,username,password,nickname,account "
					  + "FROM userinfo "
					  + "WHERE username=? AND password=?";
		PreparedStatement ps = conn.prepareStatement(sql);
		ps.setString(1,username);
		ps.setString(2, password);
		ResultSet rs = ps.executeQuery();
		if(rs.next()) {
			int id = rs.getInt("id");
			username = rs.getString("username");
		   password = rs.getString("password");
			String nickname = rs.getString("nickname");
			int account = rs.getInt("account");
			UserInfo userInfo = new UserInfo(id, username, password, nickname, account);
			return userInfo;
		}
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		DBUtil.closeConnection(conn);
	}
	return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

代码实现:UserDAO重构LoginServlet

package service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;


import core.HttpServlet;
import dao.UserInfoDAO;
import db.DBUtil;
import entities.UserInfo;
import http.HttpRequest;
import http.HttpResponse;

public class LoginServlet extends HttpServlet{
	public void service(HttpRequest request,HttpResponse response) {
		System.out.println("开始登录...");//打桩
		//获取用户名,密码
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		System.out.println(username+","+password);//打桩
		
		/*
		 * 读取userinfo.txt文件中的所有用户,并且逐个比较。
		 * 先现实功能,然后在考虑其他事,直截了当的话,首先你这读文件了,try-catch捕获异常是在所难免的。
		 */
		//先创建一个BufferedReader,按行读就可以了,当然,这个BufferedReader用完了得关掉,所以把它放到外面定义了。
		//BufferedReader br = null;
		Connection conn = null;
		try {
			//基础工作做好后,在这里初始化br,new转换流,new字节流,传入要读取得文件。
			//br = new BufferedReader(new InputStreamReader(new FileInputStream("webapp"+File.separator+"userinfo.txt")));
//			conn = DBUtil.getConnection();
			//Boolean flag = comparedNameAndPwd(br,username,password);
//			Boolean flag = comparedNameAndPwd(conn,username,password);
			//flag的值判断完成后,我们就可以在这根据它的值是否为true来决定它是否登录成功。
			UserInfoDAO dao = new UserInfoDAO();
			UserInfo userInfo = dao.findByUsernameAndPassword(username, password);
			
			if(userInfo!=null) {
//			if(flag) {
				System.out.println("登录成功!");//打桩
				/*
				 * 响应登录成功的页面给客户端(拷贝上面代码,登录失败也一样)
				 */
				forward("/login_ok.html",response);
			} else {
				System.out.println("登录失败!");
				forward("/login_error.html", response);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}/*finally {
			DBUtil.closeConnection(conn);
			//closeBr(br);
		}*/
	}

	private void closeBr(BufferedReader br) {
		//先把基础的工作都写好,在这里把BufferedReader关掉。
		if(br!=null) {
			try {
				//关流时,它要求捕获异常,我们就捕获异常。
				br.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}		
	}

	private Boolean comparedNameAndPwd(Connection conn, String username, String password) throws SQLException {
		boolean flag = false;
		String sql = "SELECT id,username,password,nickname,account "
				     + "FROM userinfo "
				     + "WHERE username=? AND password=? ";
		PreparedStatement ps = conn.prepareStatement(sql);
		ps.setString(1, username);
		ps.setString(2, password);
		ResultSet rs = ps.executeQuery();
		if(rs.next()) {
			flag = true;
		}
		return flag;
	}

	private Boolean comparedNameAndPwd(BufferedReader br, String username, String password) throws IOException {
		//然后,按行读取文件的内容,先从文件里读取一行,如果读取到的内容不为空,就说明我读到一行了。
		String line = null;
		boolean flag = false;//登录是否成功
		while((line = br.readLine())!=null) {
			System.out.println("line:"+line);//打桩
			//按照","拆分出用户名和秘密
			String[] userInfos = line.split(",");
			/*
			 * 判断用户名和秘密是否一致	
			 */
			if("".equals(username.trim())||"".equals(password.trim())) {
				flag=false;
			}
			if(username.equals(userInfos[0])&&password.equals(userInfos[1])) {
				//登录成功! 将flag改成true,即这样逐行判断,只要有一个对了,就将flag改成true,之后就break,后面的就不用再读取判断了。
				flag = true;
			}
		}
		return flag;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113

代码实现:update.html,update_ok.html,update_info.html


<html>
	<head>
		<meta charset="UTF-8">
		<title>修改用户</title>
	</head>
	<body>
		<center>
			<h1>用户修改</h1>
			<form action="update" method="get">
				<table border="1">
					<tr>
						<td>用户名:</td>
						<td><input name="username" type="text" size="30"/></td>
					</tr>
					<tr>
						<td>密  码:</td>
						<td><input name="password" type="password" size="30"/></td>
					</tr>
					<tr>
						<td>昵  称:</td>
						<td><input name="nickname" type="text" size="30"/></td>
					</tr>
					<tr>
						<td>余  额:</td>
						<td><input name="account" type="text" size="30"/></td>
					</tr>
					<tr>
						<td colspan="2" align="center"><input type="submit" value="修改"></td>
					</tr>
				</table>
			</form>
		</center>
	</body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

代码实现:Userinfo.update(…)

/**
 * 修改给定的用户信息
 * @param userInfo
 * @return
 */
public boolean update(UserInfo userInfo) {
	/*
	 * 名字和ID不可修改,可以根据用户名
	 * 修改usserInfo中该用户的新密码,昵称
	 * 以及余额
	 * UPATE userinfo
	 * SET password=?,nickname=?,account=?
	 * WHERE username=?
	 */
	Connection conn = null;
	try {
		conn = DBUtil.getConnection();
		String sql = "UPDATE userinfo SET "
				     + "password=?,nickname=?,account=? "
					 + "WHERE "
				     + "username=?";
		PreparedStatement ps = conn.prepareStatement(sql);
		ps.setString(1, userInfo.getPassword());
		ps.setString(2, userInfo.getNickname());
		ps.setInt(3, userInfo.getAccount());
		ps.setString(4, userInfo.getUsername());
		int d = ps.executeUpdate();
		if(d>0) {
			//修改数据成功
			return true;
		}
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		DBUtil.closeConnection(conn);
	}
	return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

<html>
	<head>
		<meta charset="UTF-8">
		<title>不存在</title>
	</head>
	<body>
		<center>
			<h1>修改失败!用户名不存在或错误的输入!</h1>	
		</center>
	</body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

<html>
	<head>
		<meta charset="UTF-8">
		<title>成功</title>
	</head>
	<body>
		<center>
			<h1>修改成功!</h1>	
		</center>
	</body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

代码实现:UpdateServlet.java

package service;

import core.HttpServlet;
import dao.UserInfoDAO;
import entities.UserInfo;
import http.HttpRequest;
import http.HttpResponse;

/**
 * 完成用户修改信息的请求
 * @author soft01
 *
 */
public class UpdateServlet extends HttpServlet{
	public void service(HttpRequest request,HttpResponse response) {
		/*
		 * 1:获取用户输入的用户名等信息
		 * 2:先判断该用户是否存在,不存在则
		 * 	  提示用户(跳转没有该用户的提示页面)
		 * 3:将用户输入的信息存入一个UserInfo
		 *    实例中
		 * 4:调用UserInfoDAO的update进行修改
		 * 5:若修改成功跳转修改成功的提示页面
		 *    否则跳转失败的提示页面
		 */
		//获取用户名,密码,昵称
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		String nickname = request.getParameter("nickname");
		String account = request.getParameter("account");
		try {
			Integer.valueOf(account.trim());	
		} catch(NumberFormatException e) {
			e.printStackTrace();
			System.out.println("accout转型失败!");
			try {
				forward("/update_info.html", response);
			} catch (Exception e1) {
				e1.printStackTrace();
			}
		}
		
		UserInfoDAO dao = new UserInfoDAO();
		//先验证该用户是否不存在?
		if(dao.findByUsername(username)==null||"nothing".equals(username)||"nothing".equals(password)||"nothing".equals(nickname)) {
			//该用户不存在!
			try {
				forward("/update_info.html", response);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}else {
			//创建一个UserInfo实例用于表示该注册信息
			UserInfo userInfo = new UserInfo();
			userInfo.setUsername(username);
			userInfo.setPassword(password);
			userInfo.setNickname(nickname);
			userInfo.setAccount(Integer.parseInt(account.trim()));
			
			//保存用户信息		
			boolean success =  dao.update(userInfo);
			try {
				if(success) {
						/*
						 * 响应修改成功的页面给客户端
						 */
						forward("/update_ok.html", response);
					}else {
						//跳转注册失败页面
						System.out.println("数据库插入失败!");
						forward("/update_info.html", response);
					}
			} catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

代码实现:ClientHandler.java

package core;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;


import common.HttpContext;
import http.HttpRequest;
import http.HttpResponse;
import service.LoginServlet;
import service.RegServlet;
import service.UpdateServlet;

/**
 * 该线程任务用于处理每个客户端的请求。
 * @author fhy
 *
 */
public class ClientHandler implements Runnable {

	private Socket socket;
	public ClientHandler(Socket socket) {
		this.socket = socket;
	}	
	
	public void run() {	
		try {
			//System.out.println("进入到run方法!");
			InputStream in = socket.getInputStream();
			//创建对应的请求对象
			//System.out.println("已经从socket中获取到输入流in:"+in);
			HttpRequest  request = new HttpRequest(in);
			
			OutputStream out = socket.getOutputStream();
			//创建对应的响应对象
			HttpResponse response = new HttpResponse(out);

			/*
			 * 处理用户请求
			 * 0.获取用户请求资源路径:/index.html
			 */
			String uri = request.getUri();
			System.out.println("uri:webapp"+uri);
			//正常我们访问一个网站,只敲一个域名(不需要写资源路径),
			//比如www.baidu.com,它就会自动跳转到首页,我们也这样做。
			if("/".equals(uri)) {
				//去首页
				File file = new File("webapp/index.html");
				responseFile(HttpContext.STATUS_CODE_OK,file,response);
			}else {
				File file = new File("webapp"+uri); 
				if(file.exists()) {
					System.out.println("找到了相应资源"+file.length());
					/*
					 * 响应页面要向用户发送的内容:
					 * HTTP/1.1 200 OKCRLF
					 * Content-Type:text/htmlCRLF
					 * Content-Type:273CRLF
					 * CRLF
					 * 1010100101001011010101010111110010010(index.html数据)
					 */
					responseFile(HttpContext.STATUS_CODE_OK,file,response);
				    //查看是否请求一个功能,先看它请求是不是一个注册if("/reg".equals(uri)),如果要是注册的话就要先办法把用户注册信息写到文件里。
				} else if("/reg".equals(uri)) {
					//调RegServlet类中的service方法,让它去处理这个请求。首先第一件事,得先实例化出来RegServlet这个类的对象。
					RegServlet servlet = new RegServlet();
					//然后调用它的service方法,并把request对象和请求对象response传给这个方法作为参数。
					servlet.service(request,response);
				} else if("/login".equals(uri)) {
					//调LoginServlet类中的service方法,让它去处理这个请求。首先第一件事,得先实例化出来LoginServlet这个类的对象。
					LoginServlet servlet = new LoginServlet();
					//然后调用它的service方法,并把request对象和请求对象response传给这个方法作为参数。
					servlet.service(request,response);
				} else if("/update".equals(uri)) {
					UpdateServlet servlet = new UpdateServlet();
					servlet.service(request,response);
				} else {
					System.out.println("没有资源:404");
					file = new File("webapp/404.html");
					responseFile(HttpContext.STATUS_CODE_NOTFOUND,file,response);
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				//如果不关流logo图片将出不来。
				socket.close();
			}catch (Exception e) {
				e.printStackTrace();
			}
		}			
	}
	
	/**
	 * 根据给定的文件分析其名字后缀以获取对应的ContentType
	 * @param file
	 * @return
	 */
	private String getContentTypeByFile(File file) {
		//获取文件名
		String name = file.getName();
		System.out.println("文件名:"+name);//打桩
		//截取后缀
		String ext = name.substring(name.lastIndexOf(".")+1);
		System.out.println("后缀:"+ext);//打桩
	    //获取对应的ContentType
		String contentType = HttpContext.contentTypeMapping
				.get(ext);
		System.out.println("contentType:"+contentType);
		return contentType;
	}

	/**
	 * 相应客户端指定资源
	 * @param status 响应状态码
	 * @param file 要响应的资源
	 * @throws Exception 
	 */
	private void responseFile(int status, File file,HttpResponse response) throws Exception {
		try {
			//1 设置状态行信息
			response.setStatus(status);
			//2 设置响应头信息
			//分析该文件后缀,根据后缀获取对应的ContentType
			response.setContentType(getContentTypeByFile(file));
			response.setContentLength((int)file.length());
			//3 设置响应正文
			response.setEntity(file);
			//4 响应客户端
			response.flush();
		} catch (Exception e) {
			throw e;
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139

写在后面

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

闽ICP备14008679号