Spring征服数据库

简介:

DAO:数据访问对象(data access object)的缩写。


如何你曾经编写过JDBC代码,你肯定会意识到如果不强制捕获SQLException,你几乎不能使用JDBC做任何事情。SQLException表示在尝试访问数据库时出现了问题,但是这个异常却没有告诉你哪里出错了以及如何进行处理。

可能导致抛出SQLException的常见问题包括:

1、应用程序无法连接数据库。

2、要执行的查询有语法错误。

3、查询中所使用的表和(或)列不存在。

4、试图插入或更新的数据违反了数据库的完整性约束。


它不是对每种可能的问题都会有不通的异常类型。而都是抛出SQLException。

一方面,JDBC的异常体系过于简单了---实际上,它算不上一个体系。另一方面,hibernate的异常体系是其本身所独有的。我们需要的是,数据访问异常要具有描述性而且又与特定的持久化框架无关。


spring几乎为读取和写入数据库的所有错误都提供了异常。Spring的数据访问异常很多。尽管Spring的异常体系比JDBC简单的SQLException丰富的多,但它没有与特定的持久化方式相关联。这意味着我们可以使用Spring抛出一致的异常,而不用关心所选择持久化方案。


Spring的异常大多继承自DataAccessException。这个异常的特殊指出在于它是一个非检查型异常。说白了不用写try catch。这把是否捕获异常的权利留给了开发人员。


Spring提供的数据访问模版,分别适用于不通的持久化机制。

下面分别是模版类和它的用途:

jca.cci.core.CciTemplate : JCA CCI连接

jdbc.core.JdbcTemplate : JDBC连接

jdbc.core.namedparam.NamedParameterJdbcTemplate : 支持命名参数的JDBC连接

jdbc.core.simple.SimpleJdbcTemplate : 通过java5简化后的JDBC连接

orm.hibernate.HibernateTemplate : Hibernate 2.x的Session

orm.hibernate3.HibernateTemplate : Hibernate 3.x的Session

orm.ibatis.SqlMapClientTemplate : iBATIS SqlMap客户端

orm.jdo.JdoTemplate : Java数据对象实现

orm.jpa.JpaTemplate : Java持久化API的实体管理器


配置数据源

1、使用JNDI数据源

Spring应用程序经常部署在Java EE应用服务器中,如WebSphere、JBoss或者像Tomcat这样的Web容器。这些服务器允许你配置通过JNDI获取数据源。这种配置的好处在于数据源完全可以在应用程序之外进行管理,这样应用程序只需在访问数据库的时候查找数据源就可以了。

使用<jee:jndi-lookup>元素装配到Spring中。

[html]  view plain  copy
 print ?
  1. <!-- 其中jndi-name属性用于指定JDDI中资源的名称。如果只设置了jndi-name属性,那么就会根据指定的名称  
  2.         查找数据源。但是,如果应用程序运行在Java应用程序服务器中,则需要将resource-ref属性设置为true,  
  3.         这样给定的jndi-name将会自动添加java:comp/env/前缀  
  4.      -->  
  5.     <jee:jndi-lookup jndi-name="/jdbc/SpitterDS" id="dataSource" resource-ref="true" />  


2、使用数据源连接池

[html]  view plain  copy
 print ?
  1. <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">  
  2.         <property name="driverClassName" value="org.hsqldb.jdbcDriver" />  
  3.         <property name="url" value="jdbc:hsqldb:hsql://localhost/spitter.spitter" />  
  4.         <property name="username" value="sa" />  
  5.         <property name="password" value="" />  
  6.         <property name="initialSize" value="5" />  
  7.         <property name="maxActive" value="10" />  
  8.     </bean>  

前4个属性是配置BasicDataSource所必需的。下面列出了另外的最有用的一些属性。

initialSize:池启动时创建的连接数量。

maxActive:同一时间可从池中分配的最多连接数。如果设置为0,表示无限制。

maxIdle:池里不会被释放的最多空闲连接数。如果设置为0,则表示无限制。

maxOpenPreparedStatements:在同一时间能够从语句池中分配的预处理语句的最大数量。如果设置为0,表示无限制。

maxWait:在抛出异常之前,池等待连接回收的最大时间(当没有可用连接时)。如果设置为-1,表示无限等待。

minEvictableIdleTimeMillis:连接在池中保持空闲而不回收的最大时间。

minIdle:在不创建新连接的情况下,池中保持空闲的最小连接数。

poolPreparedStatements:是否对预处理语句进行池处理(布尔值)


3、基于JDBC驱动的数据源

在Spring中,通过JDBC驱动定义数据源是最简单的配置方式。Spring提供了两种数据源对象。

DriverManagerDataSource:在每个连接请求时都会返回一个新建的连接。与DBCP的BasicDataSource不同,由DriverManagerDataSource提供的连接兵没有进行池化管理。

SingleConnectionDataSource:在每个连接请求时都会返回同一个连接。尽管SingleConnectionDataSource不是严格意义上的连接池数据源,但是你可以将其视为只有一个连接的池。

以上两个数据源的配置与BasicDataSource的配置类似:

[html]  view plain  copy
 print ?
  1. <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">  
  2.         <property name="driverClassName" value="org.hsqldb.jdbcDriver" />  
  3.         <property name="url" value="jdbc:hsqldb:hsql://localhost/spitter.spitter" />  
  4.         <property name="username" value="sa" />  
  5.         <property name="password" value="" />  
  6.     </bean>  

唯一的区别在于DriverManagerDataSource和SingleConnectionDataSource都没有提供连接池功能,所以没有可配置的池相关的属性。

SingleConnectionDataSource有且只有一个数据库连接,所以不适用于多线程的应用程序。尽管DriverManagerDataSource支持多线程,但是在每次请求连接时都会创建新连接,这是以性能为代价的。鉴于以上的这些限制,我强烈建议应该使用数据源连接池。


使用JDBC模版

Spring为JDBC框架承担了资源管理和异常处理的工作,从而简化了JDBC代码,让我们只需编写从数据库读写数据的必须代码。

Spring将数据访问的样板式代码提取到模版类中。Spring为JDBC提供了3个模版类供使用。

1、JdbcTemplate:最基本的Spring的JDBC模版,这个模版支持最简答你的JDBC数据库访问功能以及简单的索引参数。

2、NamedParameterJdbcTemplate:使用该模版类执行查询时,可以将查询值以命名参数的形式绑定到SQL中,而不是使用简单的索引参数。

3、SImpleJdbcTemplate:该模版类利用Java5的一些特性,例如自动装箱、泛型以及可变参数列表来简化JDBC模版的使用。


使用SImpleJdbcTemplate访问数据

[html]  view plain  copy
 print ?
  1. <bean id="jdbcTemplate" class="org.springframework.jdbc.core.simple.SimpleJdbcTemplate">  
  2.         <constructor-arg ref="dataSource"></constructor-arg>  
  3.     </bean>  

现在,可以将jdbcTemplate装配到DAO中兵使用了 SImpleJdbcTemplate:

[html]  view plain  copy
 print ?
  1. public class JdbcSpitterDAO implements SpitterDAO {  
  2.     private SimpleJdbcTemplate jdbcTemplate ;  
  3.     public void setJdbcTemplate(SimpleJdbcTemplate jdbcTemplate) {  
  4.         this.jdbcTemplate = jdbcTemplate ;  
  5.     }  
  6. }  

你还需要装配JdbcSpitterDAO的jdbcTemplate属性,如下:

[html]  view plain  copy
 print ?
  1. <bean id="spitterDao" class="com.habuma.spitter.persistence.SimpleJdbcTemplateSpitterDao">  
  2.         <property name="jdbcTemplate" ref="jdbcTemplate" />  
  3.     </bean>  


下面是基于SimpleJdbcTemplate的addSpitter()方法。

[html]  view plain  copy
 print ?
  1. public void addSpitter(Spitter spitter) {  
  2.         jdbcTemplate.update(SQL_INSERT_SPITTER,   
  3.                 spitter.getUsername(),  
  4.                 spitter.getPassword(),  
  5.                 spitter.getFullName(),  
  6.                 spitter.getEmail(),  
  7.                 spitter.isUpdateByEmail()) ;  
  8.         spitter.setId(queryForIdentity()) ;  
  9.     }  

使用JdbcTemplate也简化了数据读取操作。下面是个新方法,在这个方法中使用SimpleJdbcTemplate回调将结果集映射成域对象。

[html]  view plain  copy
 print ?
  1. public Spitter getSpitterById(long id) {  
  2.         return jdbcTemplate.queryForObject(SQL_SELECT_SPITTER_BY_ID,   
  3.                     new ParameterizedRowMapper<Spitter>() {  
  4.                         @Override  
  5.                         public Spitter mapRow(ResultSet rs, int rowNum)  
  6.                                 throws SQLException {  
  7.                             Spitter spitter = new Spitter() ;  
  8.                             spitter.setId(rs.getLong(1)) ;  
  9.                             spitter.setUsername(rs,getString(2)) ;  
  10.                             spitter.setPassword(rs.getString(3)) ;  
  11.                             spitter.setFullName(rs.getString(4)) ;  
  12.                             return spitter ;  
  13.                             return null;  
  14.                         }  
  15.                     }, id) ;  
  16.     }  

在getSpitterById()方法中使用了SimpleJdbcTemplate的queryForObject()方法来从数据库查询Spitter。queryForObject()方法有3个参数:

1、String,包含了要从数据库中查找数据的SQL。

2、parameterizedRowMapper对象,用来从ResultSet中提取值并构建域对象

3、可变参数列表,列出了要绑定到查询上的索引参数值。


使用命名参数

在上面的addSpitter()方法使用了索引参数。这意味着我们需要留意查询中参数的顺序,而在将值传递给update()方法的时候要保持正确的顺序。如果在修改SQL时更改了参数的顺序,那么我们还需要修改参数值的顺序。

除了这种方法之外,我们还可以使用命名参数。命名参数可以赋予SQL中的每个参数一个明确的名字,在绑定值到查询语句的时候就通过该名字来引用参数。例如:

[html]  view plain  copy
 print ?
  1. private static final String SQL_INSERT_SPITTER =   
  2.             "insert into spitter(username, password, fullname) " +   
  3.             "values (:username, :password, :fullname)" ;  

使用命名参数查询,绑定值的顺序就不重要了,我们可以按照名字来绑定值。如果查询语句发生了变化导致参数的顺序域之前不一致,我们不需要修改绑定的代码。

[html]  view plain  copy
 print ?
  1. public void addSpitter(Spitter spitter) {  
  2.         Map<String, Object> params = new HashMap<String, Object>() ;  
  3.         params.put("username", spitter.getUserName()) ;  
  4.         params.put("password", spitter.getPassword()) ;  
  5.         params.put("fullname", spitter.getFullName()) ;  
  6.           
  7.         jdbcTemplate.update(SQL_INSERT_SPITTER, params) ;  
  8.         spitter.setId(queryForIdentity()) ;  
  9.     }  

使用SPring 的JDBC DAO支持类

对于应用程序中的每一个JDBC DAO类,我们都需要添加一个SimpleJdbcTemplate属性以及对应的setter方法,并确保将SimpleJdbcTemplate Bean装配到每个DAO的SimpleJdbcTemplate属性中。如果应用程序中只有一个DAO,这并不算什么问题,但是如果有多个DAO的话,这就会产生大量的重复工作。

一种可行的解决方案就是为所有的DAO创建一个通用的父类,在其中会有SimpleJdbcTemplate属性。然后让所有的DAO类继承这个类兵使用父类的SimpleJdbcTemplate进行访问。

Spring提供了内置的基类。(JdbcDaoSupport、SimpleJdbcDaoSupport和NamedParameterJdbcDaoSupported)

下面是例子:

[html]  view plain  copy
 print ?
  1. public class JdbcSpitterDao extends SimpleJdbcDaoSupport implements SpitterDao{  
  2.     ...  
  3. }  

SimpleJdbcDaoSupport通过getSimpleJdbcTemplate()能够便捷地访问SimpleJdbcTemplate。例如,addSpitter()方法可以这样改写:

[html]  view plain  copy
 print ?
  1. public void addSpitter(Spitter spitter) {  
  2.         getSimpleJdbcTemplate().update(SQL_INSERT_SPITTER,   
  3.                 spitter.getUserName(),  
  4.                 spitter.getPassword(),  
  5.                 spitter.getFUllName(),  
  6.                 spitter.getEmail(),  
  7.                 spitter.isUpdateByEmail()) ;  
  8.         spitter.setId(queryForIdentity()) ;  
  9.     }  




在Spring中继承Hibernate

在使用过jdbc后,我们还需要一些复杂的特性:

1、延迟加载:我们可以只抓去需要的数据。

2、预先抓取:这与延迟加载是相对的,借助于预先抓去,我们可以使用一个查询获取完整的关联对象。如果需要PurchaseOrder及其关联的LineItem对象,预先抓去的功能可以在一个操作中将它们全部从数据库中提取出来,这节省了多次查询的成本。

3、级联:有时,更改数据库中的表会同时修改其他表。


SPring对多个持久化框架都提供了支持,包括Hibernate、iBATIS、JAVA数据对象(Java Data Objects,JDO)以及java持久化API。

与Spring对JDBC的支持那样,Spring对ORM框架的支持提供了与这些框架的集成点以及一些附加的服务,如下所示:

1、Spring声明式事务的集成支持

2、透明的异常处理

3、线程安全的、轻量级的模版类

4、DAO支持类

5、资源管理


声明Hibernate的Session工厂

使用Hibernate的主要接口是org.hibernate.Session。Session接口提供了基本的数据访问功能,如保存、更新、删除以及从数据库加载对象的功能。通过Hibernate的Session接口,应用程序的DAO能够满足所有的持久化需求。

获取Hibernate Session对象的标准方式是借助于Hibernate的SessionFactory接口的实现类。除了一些其他的任务,SessionFactory主要负责Hibernate Session的打开、关闭以及管理。


1、XML配置

[html]  view plain  copy
 print ?
  1. <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">  
  2.         <property name="dataSource" ref="dataSource" />  
  3.         <property name="mappingResources">  
  4.             <list>  
  5.                 <value>Spitter.hbm.xml</value>  
  6.             </list>  
  7.         </property>  
  8.         <property name="hibernateProperties">  
  9.             <props>  
  10.                 <prop key="dialect">org.hibernate.dialect.HSQLDialect</prop>  
  11.             </props>  
  12.         </property>  
  13.     </bean>  

2、注解配置

[html]  view plain  copy
 print ?
  1. <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">  
  2.         <property name="dataSource" ref="dataSource" />  
  3.         <property name="packagesToScan" value="com.habuma.spitter.domain" />  
  4.         <property name="hibernateProperties">  
  5.             <props>  
  6.                 <prop key="dialect">org.hibernate.dialect.HSQLDialect</prop>  
  7.             </props>  
  8.         </property>  
  9.     </bean>  

就像在LocalSessionFactoryBean中那样,dataSource和hibernateProperties属性声明了从哪里获取数据库连接以及要使用哪一种数据库。

这里不再列出Hibernate配置文件,而是使用packagesToScan属性告诉Spring扫描一个或多个包以查找域类,这些类通过注解方式表明要使用Hibernate进行持久化。使用JPA的@Entity或@MappedSuperclass注解以及Hibernate的@Entity注解进行标注的类都会包含在内。

如果愿意,我们还可以通过使用annotatedClassed属性来将应用程序中所有的持久化类以全限定名的方式明确列出:

[html]  view plain  copy
 print ?
  1. <property name="annotatedClassed">  
  2.             <list>  
  3.                 <value>com.habuma.spitter.domain.Spitter</value>  
  4.                 <value>com.habuma.spitter.domain.Spittle</value>  
  5.             </list>  
  6.         </property>  

annotatedClassed属性对于准确指定少量的域类是不错的选择。如果你有很多的域类且不想将其全部列出。或者你想自由地添加或溢出域类而不想修改Spring配置的话,则使用packagesToScan属性更合适。


构建不依赖于Spring的Hibernate代码

[html]  view plain  copy
 print ?
  1. import org.hibernate.Session;  
  2. import org.hibernate.SessionFactory;  
  3. import org.springframework.beans.factory.annotation.Autowired;  
  4. import org.springframework.stereotype.Repository;  
  5.   
  6. @Repository  
  7. public class HibernateSpitterDao implements SpitterDao{  
  8.     private SessionFactory sessionFactory ;  
  9.     @Autowired  
  10.     public HibernateSpitterDao(SessionFactory sessionFactory) {  
  11.         this.sessionFactory = sessionFactory ;  
  12.     }  
  13.       
  14.     private Session currentSession() {  
  15.         return sessionFactory.getCurrentSession() ;  
  16.     }  
  17.       
  18.     public void addSpitter(Spitter spitter) {  
  19.         currentSession().save(spitter) ;  
  20.     }  
  21.       
  22.     public Spitter getSpitterById(long id) {  
  23.         return (Spitter)currentSession().get(Spitter.class, id) ;  
  24.     }  
  25.       
  26.     public void saveSpitter(Spitter spitter) {  
  27.         currentSession().update(spitter);  
  28.     }  
  29. }  
  30.    

我们在类上使用了@Repository注解,这会为我们做两件事情。首先,@Repository是Spring的另一种构造型注解,它能够像其他注解一样被Spring的<context:component-scan>所扫描到。这样就不必明确声明HibernateSpitterDao Bean了,只需在<context:component-scan>配置即可。

除了帮助简化XML配置以外,@Repository还有另外一个用处。让我们回想一下模版类,它有一项任务就是捕获平台相关的异常,然后以Spring的非检查型异常形式重新抛出。如果我们使用Hibernate上下文Session而不是Hibernate模版,那么异常转换会怎么处理呢?

为了给不使用模版的Hibernate DAO添加异常转换功能,我们只需在Spring应用上下文中添加一个PersistenceExceptionTranslationPostProcessorBean:


[html]  view plain  copy
 print ?
  1. <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />  

PersistenceExceptionTranslationPostProcessor是一个Bean的后置处理程序,它会在所有拥有@Repository注解的类上添加一个通知器(advisor),这样就会捕获任何平台相关的异常并以Spring的非检查型数据访问异常的形式重新抛出。



Spring与Java持久化API

配置实体管理器工厂

简单来说,基于JPA的应用程序使用EntityManagerFactory的实现类来获取EntityManager实例。JPA定义了两种类型的实体管理器:

1、应用程序管理类型:当应用程序向实体管理器工厂直接请求实体管理器时,工厂会创建一个实体管理器。在这种模式下,程序要负责打开或关闭实体管理器并在事务中对其进行控制。这种方式的实体管理器适合于不运行在Java EE容器中的独立应用程序。

2、容器管理类型:实体管理器由Java EE创建和管理。应用程序根本不与实体管理器工厂打交道。相反,实体管理器直接通过注入或JNDI来获取。容器负责配置实体管理器工厂。这种类型的实体管理器最适合用于Java EE容器,在这种情况下会希望在persistence.xml指定的JPA配置之外保持一些自己对JPA的控制。

它们两个的区别在于EntityManager的创建和管理方式。应用程序管理类型的EntityManager是由EntityManagerFactory创建的。而后者是通过PersistenceProvider的createEntityManagerFactory()方法得到的。与此相对,容器管理类型的entityManagerFactory是通过PersistenceProvider的createContainerEntityManagerFactory()方法获得的。

在容器管理的场景下,SPring会担当容器的角色。

这两种实体管理器工厂分别由对应的Spring工厂Bean创建的:

1、LocalEntityManagerFactoryBean生成应用程序管理类型的EntityManagerFactory。

2、LocalContainerEntityManagerFactoryBean生成容器管理类型的EntityManagerFactory。


使用应用程序管理类型的JPA

对于应用程序管理类型的实体管理器工厂来说,它绝大部分配置文件来源于一个名叫persistence.xml的配置文件。这个文件必须位于类路径下的META-INF目录下。

persistence.xml的作用在于定义一个或多个持久化单元。持久化单元是同一个数据源下的一个或多个持久化类。简单来讲,persistence.xml列出了一个或多个的持久化类以及一些其他的配置,如数据源和基于XML的配置文件。以下是一个典型的persistence.xml文件,它用于Spitter应用程序。

[html]  view plain  copy
 print ?
  1. <persistence xmlns="http://java.sun.com/xml/ns/persistence"  
  2.     version="1.0">  
  3.     <persistence-unit name="spitterPU">  
  4.         <class>com.habuma.spitter.domain.Spitter</class>  
  5.         <class>com.habuma.spitter.domain.Spittle</class>  
  6.         <properties>  
  7.             <property name="toplink.jdbc.driver" value="org.hsqldb.jdbcDriver" />  
  8.             <property name="toplink.jdbc.url" value="jdbc:hsqldb:hsql://localhost/spitter/spitter" />  
  9.             <property name="toplink.jdbc.user" value="sa" />  
  10.             <property name="toplink.jdbc.password" value="" />  
  11.         </properties>  
  12.     </persistence-unit>     
  13. </persistence>  

因为在persistence.xml文件中包含了大量的配置信息,所以在Spring中需要配置的就很少了。可以通过以下的<bean>元素在Spring中声明LocalEntityManagerFactoryBean:

[html]  view plain  copy
 print ?
  1. <bean id="emf" class="org.springframework.orm.jpa.LocalEntityFactoryBean">  
  2.         <property name="persistenceUnitName" value="spitterPU" />  
  3.     </bean>  

赋给persistenceUnitName属性的值就是persistence.xml中持久化单元的名称。


使用容器管理类型的JPA

容器管理的JPA采取了一种不同的方式。当在容器中运行时,可以使用容器提供的信息来生成EntityManagerFactory。

你可以将数据源信息配置在Spring应用上下文中,而不是在persistence.xml中了。例如,下面的<bean>声明展现了在Spring中如何使用LocalContainerEntityManagerFactoryBean来配置容器管理类型的JPA。

[html]  view plain  copy
 print ?
  1. <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">  
  2.         <property name="dataSource" ref="dataSource" />  
  3.         <property name="jpaVendorAdapter" ref="jpaVendorAdapter" />  
  4.     </bean>  

jpaVendorAdapter属性用于指明所使用的是哪一个厂商的JPA实现。Spring提供了多个JPA厂商适配器:

1、EclipseLinkJpaVendorAdapter

2、HibernateJpaVendorAdapter

3、OpenJpaVendorAdapter

4、TopLinkJpaVendorAdapter

目录
相关文章
|
4天前
|
安全 Java 数据库
后端进阶之路——万字总结Spring Security与数据库集成实践(五)
后端进阶之路——万字总结Spring Security与数据库集成实践(五)
|
4天前
|
SQL 监控 druid
p6spy【SpringBoot集成】使用p6spy-spring-boot-starter集成p6spy监控数据库(配置方法举例)
p6spy【SpringBoot集成】使用p6spy-spring-boot-starter集成p6spy监控数据库(配置方法举例)
247 0
|
4天前
|
安全 Java 数据库连接
在IntelliJ IDEA中通过Spring Boot集成达梦数据库:从入门到精通
在IntelliJ IDEA中通过Spring Boot集成达梦数据库:从入门到精通
|
4天前
|
存储 安全 Java
Spring Security实现基于数据库实现认证
本文档介绍了如何在Spring Security框架中基于数据库实现用户认证。首先,Spring Security提供了一个`UserDetailsService`接口,用于获取用户详细信息,通常在用户尝试登录时被调用。
51 5
|
4天前
|
容灾 Java 数据库
OceanBase数据库常见问题之spring boot应用增加了flyway的依赖但没执行如何解决
OceanBase 是一款由阿里巴巴集团研发的企业级分布式关系型数据库,它具有高可用、高性能、可水平扩展等特点。以下是OceanBase 数据库使用过程中可能遇到的一些常见问题及其解答的汇总,以帮助用户更好地理解和使用这款数据库产品。
|
6月前
|
存储 Java 数据库
java spring boot 数据库密码解密
java spring boot 数据库密码解密
|
6月前
|
存储 Java 数据库连接
Spring Boot 配置主从数据库实现读写分离
Spring Boot 配置主从数据库实现读写分离
204 0
|
4天前
|
SQL Java 数据库连接
(数据库链接池)spring内容复习7月16日笔记
(数据库链接池)spring内容复习7月16日笔记
16 0
|
4天前
|
Java 数据库 Nacos
spring-gateway基于数据库 + nacos 的动态路由
spring-gateway基于数据库 + nacos 的动态路由
98 0
|
4天前
|
前端开发 JavaScript Java
基于spring+jsp+mysql实现的Java web论坛系统【源码+数据库+指导运行】
基于spring+jsp+mysql实现的Java web论坛系统【源码+数据库+指导运行】