简介:面向Java Web开发人员的框架整合实践源代码包,以J2EE、Struts、Hibernate、Spring四大主流技术为主线,适合希望掌握SSH整合开发、理解MVC分层与ORM映射的中级以上学习者。压缩包共1145个文件,包含218个Java源文件、243个class编译文件、138个jsp页面、93个xml配置、62个tld标签库以及properties、sql、db等辅助文件,完整覆盖模型类、Action、Form Bean、Servlet/JSP和struts-config.xml、hibernate.cfg.xml、applicationContext.xml等关键配置,包体约2.35MB。资源通过可运行示例展现各框架协作方式,帮助读者从代码层面理解依赖注入、事务管理、持久化操作及前后端交互,也可作为项目搭建和排错参考。已有394人学习下载,适合自学研读或课堂配套使用。
1. 为什么一份整合源代码,比三十集视频更能帮你突击
如果只看标题“Java Web主流框架整合开发源代码”,你可能会以为它只是一堆能跑的demo堆在一起。但真正干过整合开发的人都知道,坑全在配置文件里,不在代码里。单独跑SpringMVC的示例很顺,单独跑MyBatis的示例也很顺,一旦把两者放进同一个工程,容器谁先创建、Bean谁该注入谁、事务边界画在哪一层,每一个环节都能让新手卡上一整天。整合这件事最值钱的不是“能跑”,而是“每个文件为什么存在”。这套源码的价值就在这里:依赖怎么配、配置怎么分、Controller到Mapper怎么串,全部摊开给你看。我按“原理 → 工程 → 配置 → 业务 → 避坑 → 进阶”的顺序把它讲透。
2. 为什么所有整合都绕不开Spring:先看懂那层“胶水”
2.1 IoC容器才是整合的基座:不是框架互相调用,而是统一交给Spring管辖
整合的第一性原理是:SpringMVC、MyBatis、业务代码之间并不直接互相调用,它们全部作为Bean注册进Spring IoC容器,由容器负责创建和注入。SpringMVC的DispatcherServlet从容器里取Controller,Controller注入Service,Service注入Mapper接口,而Mapper接口本身是被MyBatis-Spring扫描后注册成的一个代理Bean。这个链条里没有一处是“new”出来的,全部是容器给的关系。
这套机制在Web工程里还有一个容易忽略的双容器模型。ContextLoaderListener负责创建根容器,里面放数据源、Service、Dao、事务管理器;DispatcherServlet启动时再创建一个SpringMVC子容器,里面放Controller和视图解析器。子容器能看见父容器的Bean,父容器看不见子容器的。所以配置放错了容器,运行时的现象就是Controller里注入的Service为null,或者启动时直接报NoSuchBeanDefinitionException。明白这个模型,你再去看“配置文件怎么分”的问题,答案已经出来一半了。
2.2 AOP把事务和日志变成了配置项:整合后的事务不再由你手动提交
抛开AOP谈整合是学不透的。声明式事务本质上是Spring AOP在做代理:你调用一个标注了@Transactional的public方法,代理会在方法进入前开启事务,方法正常返回时提交,抛出异常时回滚。MyBatis的SqlSession也由Spring统一管理,你不再需要自己openSession、手动commit、finally里close,SqlSessionTemplate会跟着事务的生命周期自动开合。
这也是整合方案与“硬编码使用MyBatis”最本质的区别。硬编码时你要写sessionFactory.openSession()和session.commit(),整合之后这些都从代码里消失了。如果你在代码里看到有人还在手动调用SqlSession的commit,那通常意味着他没有把SqlSession交给Spring,事务管理十有八九是错位的。日志AOP同理:你可以用切面统一打印Controller入参、Service耗时、异常堆栈,而不是在每个业务方法里重复写日志调用。
2.3 SSM与SSH两大流派:新项目怎么选,老工程怎么迁
标题里的“主流框架”在Java Web领域最有代表性的是两组:SSH和SSM。SSH指Struts2加Spring加Hibernate,SSM指SpringMVC加Spring加MyBatis。两组都用到了Spring作为整合底座,区别主要在于前端控制器和持久层框架。
| 对比项 | SSM(SpringMVC + Spring + MyBatis) | SSH(Struts2 + Spring + Hibernate) |
|---|---|---|
| 控制器粒度 | 方法级注解,一个方法对应一个请求 | 类级Action + XML配置,一个类对应一类动作 |
| 持久层方式 | 手写SQL,半自动映射,SQL完全可控 | HQL和ORM自动映射,全自动但SQL难以精细化控制 |
| 学习曲线 | 中等偏下,SQL熟练就能上手 | 较陡,Hibernate的映射、缓存、懒加载概念多 |
| 适用场景 | 新项目、SQL复杂的业务系统、报表类应用 | 存量系统、领域模型复杂的传统业务平台 |
| 维护现状 | 新项目主流,资料多,招人容易 | 存量老系统居多,维护成本高,迁移需求大 |
我的建议很直接:新工程一律走SSM这条线,理由不是SSH技术差,而是现在的新项目、团队协作、问题排查资料几乎都沉淀在SSM这一侧。真遇到SSH老系统的整合改造,也不要急着推翻重写,Spring的核心知识完全复用,把Struts2的Action迁移成SpringMVC的Controller,把Hibernate的实体映射迁到MyBatis的XML,按模块灰度替换即可。下面所有具体操作,我以SSM为例展开,这也是这份整合源代码的默认主线路。
3. 从零搭一个能跑的整合工程:Maven依赖、web.xml与三份配置
3.1 多模块Maven工程怎么拆:依赖隔离是第一道防线
整合工程不要做成单模块WAR包一坨到底。常见的做法是拆成四个模块:demo-core放实体和公共工具,demo-dao放Mapper接口与XML,demo-service放业务逻辑和事务,demo-web放Controller、配置文件和webapp目录。父工程demo-parent只做两件事:声明子模块,统一管理依赖版本。
父工程pom.xml的关键部分长这样:
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>demo-parent</artifactId> <version>1.0.0</version> <packaging>pom</packaging> <modules> <module>demo-core</module> <module>demo-dao</module> <module>demo-service</module> <module>demo-web</module> </modules> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.x</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.x</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.x</version> </dependency> </dependencies> </dependencyManagement> </project>这里的关键是dependencyManagement标签。它的作用不是引入依赖,而是锁版本:子模块里只需写groupId和artifactId,版本号全部继承父工程。这样整个项目的Spring全家桶版本、MyBatis版本、Jackson版本只要在父工程里改一处,全局生效,从根上避免“子模块之间版本打架”的传递依赖冲突。
各模块的依赖职责分配如下:
| 依赖 | 所属模块 | 用途 |
|---|---|---|
| spring-webmvc | demo-web | MVC框架、DispatcherServlet |
| spring-jdbc | demo-dao | 数据源、事务管理器基础设施 |
| mybatis | demo-dao | 持久层核心 |
| mybatis-spring | demo-dao | MyBatis与Spring的装配桥接 |
| mysql-connector-java | demo-dao | MySQL JDBC驱动 |
| druid | demo-dao | 数据源与连接池监控 |
| jackson-databind | demo-web | JSON序列化与反序列化 |
| javax.servlet-api | demo-web | Servlet编译期依赖,scope置provided |
| log4j2 + slf4j | 全部模块 | 日志门面与实现 |
我见过不少工程把这些依赖全部堆在web层,dao层编译时找不到mybatis的类,启动时才被Tomcat类加载救回来,这是典型的模块边界失守。dao模块必须在自己的pom里直接引用mybatis和mysql驱动,而不是依赖web模块传递过来。
3.2 web.xml做了三件事:编码、根容器、前端控制器
SSM整合的web.xml只关心三件事:CharacterEncodingFilter保证请求和响应编码,ContextLoaderListener创建根容器,DispatcherServlet创建MVC子容器。加载顺序由web.xml里的元素顺序决定,这个顺序错一个字,结果都不同。
<web-app> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>这里最容易翻车的两个点。第一,url-pattern要写“/”而不是“/”,写“/”会把对JSP的转发请求也拦进DispatcherServlet,导致页面直接404或变成一堆乱码;写“/”则把.jsp的转发交给了容器默认Servlet。第二,load-on-startup必须配1,让DispatcherServlet在应用启动时就创建子容器,否则第一次请求才初始化,启动阶段的反常现象会推迟到运行期才暴露。CharacterEncodingFilter的forceEncoding设为true,是为了让“请求和响应两侧都强制用UTF-8”,而不是只改请求一侧。
3.3 三份配置各自的边界:根容器、MVC容器、MyBatis自身
applicationContext.xml归属根容器,职责是数据源、事务、Mapper扫描、Service扫描。
<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="typeAliasesPackage" value="com.demo.core.entity"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.demo.dao"/> </bean> <context:component-scan base-package="com.demo.service"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>逐一说关键参数。typeAliasesPackage把实体类的包名注册为别名,XML里写parameterType和resultType时可以直接用User而不是全限定名,少写一长串包路径。mapperLocations指向classpath:mapper/.xml,注意开头的classpath:不带星,表示在jar和classes目录下查找;如果你有多个依赖模块含有mapper文件,要把写法改成classpath:mapper/*.xml才能跨jar包匹配。MapperScannerConfigurer的basePackage只扫Mapper接口包,不扫XML——XML由上面的mapperLocations负责加载,很多整合工程启动后Mapper方法报“Invalid bound statement”就是这两处路径没对上。
spring-mvc.xml归属MVC子容器,职责是Controller扫描、注解驱动、静态资源放行、视图解析。
<context:component-scan base-package="com.demo.web.controller"/> <mvc:annotation-driven/> <mvc:default-servlet-handler/> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>component-scan这里有一个非常隐蔽的坑:如果扫描的包范围过大,比如直接扫com.demo,会把根容器里的Service再实例化一遍,Controller里注入的Service就会收到子容器这一份全新实例,事务代理还会因此失效。规范做法是MVC容器只扫controller包,根容器只扫service包,两者严格错开。mvc:default-servlet-handler让静态资源比如CSS、JS、图片在DispatcherServlet拦截后能交给容器默认Servlet处理,不配它,前端页面样式全部白屏。
mybatis-config.xml管MyBatis自身的行为,不进任何容器,只是被SqlSessionFactoryBean引用加载:
<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="SLF4J"/> </settings> </configuration>mapUnderscoreToCamelCase开启后,数据库的create_time列能自动映射到实体的createTime属性,省去大部分resultMap的字段映射;logImpl设为SLF4J,是为了让MyBatis执行SQL时把日志交给你统一的日志框架,调试时能看到每条SQL和参数。这里多说一句,JDBC连接串如果直接写死在XML里,&符号要先转义成&,所以建议把jdbc的四个参数单独放到jdbc.properties里,既绕开转义问题,也方便后续做多环境切换。
4. 走一遍用户模块的业务链路:Controller、Service、Mapper怎么分工
4.1 实体、Mapper接口与XML:先定数据边界再写业务
整合工程里最典型的一个完整闭环是用户模块的增删改查。先从数据边界开始。实体类很简单,字段对齐数据库表结构:
public class User { private Integer id; private String username; private String email; private Date createTime; // getter / setter 省略 }Mapper接口只声明方法,不写实现,MyBatis会为它生成代理:
public interface UserMapper { List<User> selectList(@Param("username") String username); User selectById(Integer id); int insert(User user); int update(User user); int deleteById(Integer id); }对应的UserMapper.xml放在资源目录的mapper文件夹下:
<mapper namespace="com.demo.dao.UserMapper"> <resultMap id="userMap" type="User"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="email" column="email"/> <result property="createTime" column="create_time"/> </resultMap> <select id="selectList" resultMap="userMap"> SELECT id, username, email, create_time FROM t_user <where> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> </where> </select> <select id="selectById" resultMap="userMap"> SELECT id, username, email, create_time FROM t_user WHERE id = #{id} </select> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> INSERT INTO t_user (username, email, create_time) VALUES (#{username}, #{email}, NOW()) </insert> <update id="update" parameterType="User"> UPDATE t_user <set> <if test="username != null">username = #{username},</if> <if test="email != null">email = #{email},</if> </set> WHERE id = #{id} </update> <delete id="deleteById"> DELETE FROM t_user WHERE id = #{id} </delete> </mapper>这里的几个细节都是踩过坑的。namespace必须和Mapper接口的全限定名一致,不一致时MyBatis扫描报错告诉你找不到statement。insert里useGeneratedKeys和keyProperty配合,数据库自增主键才会回填到传入的User对象的id属性上,否则插入后你拿不到新主键,后续关联逻辑没法写。动态SQL用 而不是手工WHERE 1=1, 会自动剔除第一个多余的AND或OR,老写法里那个1=1不仅丑,数据量大时还会影响执行计划。
4.2 Service层:事务的边界为什么必须画在这里
Service层是事务的边界。Controller不做事务,Mapper不做事务,只有业务方法需要保证多步操作的原子性时,事务注解才出现在Service实现类上:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override @Transactional(rollbackFor = Exception.class) public int createUser(User user) { userMapper.insert(user); // 其他关联业务动作 return user.getId(); } }@Transactional必须加在public方法上,加在private方法上注解被直接忽略,这是Spring代理机制决定的。rollbackFor = Exception.class这一行几乎不能省,Spring默认只对RuntimeException回滚,如果Service里抛出的是受检异常,方法结束时事务照样提交,数据库里就留下了一条你以为失败了的脏数据,这是血泪经验。
另一个容易被忽略的是同类自调用问题,也就是在UserServiceImpl内部调用另一个标了@Transactional的同类方法时,调用发生在this实例上,不会经过代理对象,事务警告无效。解决方式是拆成不同的Bean互相调用,或者自注入代理。你在走读一份整合源码时,重点看的不是Service写了多少业务逻辑,而是事务注解的摆放位置和调用链有没有绕过代理。
4.3 Controller层:页面与JSON两种返回方式怎么取舍
Controller是整合工程面向请求的入口。纯接口项目从Spring 4开始推荐@RestController,它是@Controller加@ResponseBody的合成体,方法返回值直接经过Jackson序列化成JSON:
@RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public Result<User> detail(@PathVariable Integer id) { return Result.success(userService.getUserById(id)); } @PostMapping public Result<Integer> create(@RequestBody User user) { return Result.success(userService.createUser(user)); } }这里为了接口响应的统一性,我习惯定义Result 包装结构,包含code、message、data三个字段,成功返回code=0,失败返回非零错误码和提示信息。注意@PathVariable用来绑定URL路径参数,@RequestBody用来把JSON请求体反序列化成User对象,@RequestParam则用于query string参数,三者混用时参数命名和前端约定要对齐,否则Spring解析失败直接报400。
如果你的工程还需要返回页面,比如管理后台的JSP,那Controller上就不能用@RestController,而是用@Controller配合返回字符串视图名,由InternalResourceViewResolver拼出/WEB-INF/views/xxx.jsp。同一套工程里两种Controller可以共存,接口归接口,页面归页面,只要记住接口路径统一带/api前缀,方便在SpringMVC里针对性地做拦截器和异常处理。
5. 整合避坑清单:五类症状、原因和排查路径
5.1 Bean装配与扫描路径类
现象1:启动时抛NoSuchBeanDefinitionException,提示找不到UserService或UserMapper类型的Bean,但你的类明明写了@Service或@Mapper。
原因就三选一:component-scan的basePackage包名拼错或作用域不对;Service被放在MVC子容器而根容器没扫到;根容器和子容器重复扫描导致Controller注入了错误实例。这里面第二种情况最常见——把@Service放在了MVC配置文件的component-scan里面。
解决:根容器只扫service和dao相关包,MVC容器只扫controller包。检查日志里Spring容器初始化时打印的Bean定义数量,如果Service的Bean被定义了两次,马上就能看到重复的bean name。还可以临时把日志级别调到DEBUG,看扫描器实际扫了哪些包路径。
现象2:启动正常,Controller里注入的Mapper接口为null,运行到业务方法直接NPE,控制台没有任何MyBatis相关日志。
原因:MapperScannerConfigurer的basePackage和Mapper接口所在包对不上,一个Mapper都没扫到,Spring注入时给了null;或者mapperLocations路径配的不是classpath*:,XML没加载,接口代理建立后找不到statement,运行时报Invalid bound statement。
解决:核对basePackage,它必须精确指向Mapper接口包。mapperLocations改成classpath*:mapper/*.xml,防止多模块场景下漏加载。如果你的Mapper接口上有@Mapper注解,且同时用了MapperScannerConfigurer,注解是冗余但不冲突;如果只用了独立XML的方式,接口上一个注解都不需要。
5.2 事务与页面路由类
现象3:Service方法里故意抛了异常,数据库数据还是被写进去了,事务像假的一样。
原因有四个:@Transactional加在非public方法上被代理忽略;同类内部自调用绕过了代理对象;方法内异常被try/catch吞了,事务根本不知道出了问题;rollbackFor没配,受检异常走了默认的提交路径。我见过最邪门的是有人在Controller方法上标了@Transactional,看起来万事大吉,实际因为Controller的代理方式和事务管理器不匹配,事务完全没生效。
解决:事务注解只写Service实现类的public方法上,rollbackFor一律配Exception.class。代码里禁止用catch块“消化”业务异常,要处理就记录日志后继续往外抛。验证方式是看Spring事务日志:在log4j2配置里把org.springframework.jdbc.datasource.DataSourceTransactionManager设为DEBUG,操作一次失败场景,日志里没出现“Rolling back”就是没回滚。
现象4:部署到Tomcat后访问首页404,但直接请求Controller的接口路径能返回JSON。
原因:DispatcherServlet的url-pattern配成了/*把JSP转发拦了;或者IDE里部署的Artifact没有把webapp目录打包进WAR;或者InternalResourceViewResolver的prefix配的是/views/,实际页面放在/WEB-INF/views/下。
解决:url-pattern只写“/”。检查部署后的目录结构,WAR里确定存在WEB-INF/views/以及对应JSP文件。视图解析器从“/”开头的路径都是从应用上下文根开始的,写成相对路径会拼在你的请求URI后面,越拼越长。
5.3 中文编码与连接串类
现象5:插入中文后数据库变问号,按中文条件查询查不到数据,但英文和数字完全正常。
原因:CharacterEncodingFilter位置放得不对,Tomcat默认的请求解码发生在Filter之前;或者JDBC连接串缺了characterEncoding=utf8参数;或者MySQL驱动版本和数据库版本不匹配,旧驱动类被新连接协议拒绝。
解决:编码Filter必须是filter-mapping里第一个执行的,谁在前谁先解码。JDBC的URL标准写法是serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false,注意&在XML里要转义成&,所以我建议写在jdbc.properties里避开转义。MySQL 8及以上必须用com.mysql.cj.jdbc.Driver这个类名,旧类名只适用5.x驱动,这一条在新环境里翻车率极高。
6. 让整合工程长出专业感:分页拦截器、Druid监控与五步验证习惯
6.1 分页:MyBatis拦截器把SQL改写成本地方言
列表查询的分页不要手写LIMIT,更不要在Java代码里把数据全查出来再切片。常见做法是引入PageHelper插件,它的原理是拦截Executor的query方法,在执行前自动把原SQL改写成一条count查询和一条带LIMIT的查询,并且通过ThreadLocal记录分页参数。
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.x</version> </dependency>使用时在查询前一行代码开启分页,后面紧跟的第一次MyBatis查询生效:
PageHelper.startPage(pageNum, pageSize); List<User> list = userMapper.selectList(username); PageInfo<User> page = new PageInfo<>(list);注意PageHelper的方法调用和查询之间不要插入其他数据库操作,否则分页参数可能被上一个查询消费掉。PageInfo里封装了total、pages、pageNum、pageSize、list这些完整信息,直接把整个对象返回给前端就行。
6.2 Druid监控与多环境配置
数据源选了Druid,它的监控页面是排查慢SQL和连接池泄漏的利器。在web.xml里注册一个StatViewServlet,映射到/druid/*,启动后访问contextPath/druid/index.html就能看到SQL执行次数、耗时分布、活跃连接数。上线前把慢SQL阈值调低,比如执行超过500ms的SQL自动标红,逐条优化。
多环境配置的做法是准备三份jdbc.properties后缀文件,或者用Spring的profile机制配多个property-placeholder。我习惯用占位符方案:配置文件里写jdbc.url=${DB_URL},部署时通过环境变量注入,这样代码仓库里不存任何环境的真实密码,切换环境时只需要改变量,不用改工程重新打包。
6.3 拿到一份整合源码的五步验证习惯
我拿到任意一份整合工程源码,第一件事永远不是点运行,而是按下面顺序读文件:先开web.xml,按listener、filter、servlet的顺序读装配逻辑,确认容器启动链路;再开applicationContext.xml,核对数据源、Mapper扫描、事务管理器三件事;然后开spring-mvc.xml,看Controller扫描范围和视图解析器;接着选一条典型的业务请求,从Controller一路走读到Mapper XML里的SQL;最后配上Druid监控和日志框架,跑一遍增删改查验证。这套顺序走完,工程里有没有埋雷基本心里有数了。很多开发者的习惯是看到Tomcat启动成功就宣布“跑通了”,然后业务一调就404,又回到视频里找答案。整合开发和单个框架最大的不同是,它的运行结果由配置文件之间的协作关系决定,而这个协作关系只能通过“读配置的顺序感”建立起来。希望帮到你。
本文还有配套的精品资源,点击获取