简介:一套整合Spring 5.1.5、SpringMVC 5.1.5、SpringSecurity 5.1.4和Hibernate 5.3.7的Web基础工程,解决Java开发者在搭建企业级商业系统时常见的框架配置繁琐、权限体系缺失问题,尤其适合需要快速交付的管理类项目。工程前后端分层清晰,前端基于Bootstrap并兼容jQuery与AJAX,页面采用SmartAdmin风格,可切换六种皮肤,交互以对话框操作为主;后端已内置用户、角色、权限模块,附MySQL建库脚本及资源数据、角色数据、管理员数据,导入数据库并启动工程即可运行,大幅降低起步成本。压缩包共包含786个文件,总大小约16.03MB,其中JS脚本324个、PNG图片174张、JPG图片61张、CSS样式32个、HTML页面28个、JSP页面18个以及Java源码16个,JS与CSS支撑SmartAdmin前端交互及皮肤切换,JSP与Java源文件承载控制器、服务层和安全配置逻辑,SQL脚本用于初始化基础数据。当前已有349人学习下载,拿到后既可直接在此框架上扩展业务模块,也可借助其完整的用户、角色、权限链路,深入学习SpringMVC与Hibernate的项目整合细节,适合中级及以上Java开发者参考使用。
1. 这套老牌组合为什么到今天还能撑起商业系统
SpringMVC 5、Spring 5、SpringSecurity 5、Hibernate 5,四个框架全部锁定在 5.x 时代,再用 Maven 串成一个 war 包——在 SpringBoot 大行其道的今天,看起来像是逆潮流而动。但不少做企业系统的团队反而主动退回这套组合:客户要求部署到已有的 Tomcat,不能接受内嵌容器;代码库沉淀了十几年的 XML 配置和拦截器;团队对 SpringBoot 的自动配置不敢全盘信任,宁可把每个 Bean 的来龙去脉握在自己手里。这套工程的好处是边界清楚:Maven 管依赖,Spring 管对象,Hibernate 管持久化,Security 管访问控制,每层都能单独排查、单独替换。
它适合两类人:一类是要接手老项目维护或升级的开发者,另一类是刚把 Spring 和 Hibernate 从 3.x/4.x 迁到 5.x、正被版本兼容问题来回折腾的团队。很多人以为“既然都是 5,依赖直接拉最新就好”,实际把四个框架放进同一个 war 包后,版本冲突、命名策略、事务边界、CSRF 拦截会让部署立刻翻车。这篇笔记只讲一件事:怎么把这套骨架搭得能用于真实商业开发,以及搭的过程中哪些坑值得提前避开。
2. 建 Maven 骨架:版本兼容矩阵与依赖清单,先写对 pom.xml 再谈代码
2.1 为什么四个框架都选 5.x 的最终维护线
Spring 5.3 是 Spring 5 系列的最终维护主线,SpringSecurity 5.8 也在持续维护,Hibernate 的 5.6 系列同样是 Hibernate 5 的收尾版本。选这条线的理由不是“最新”,而是“最后”:它们在同一代 API 里修完了最多的缺陷,javax.servlet、javax.persistence 这些命名空间保持一致,不会像 Hibernate 6 那样全面切到 jakarta 让人措手不及。
版本兼容的关键在于 Spring 对 Hibernate 5 的适配层。Spring 5.3 的 spring-orm 模块内置了针对 Hibernate 5 的 LocalSessionFactoryBean 和 HibernateTransactionManager,这两个类是你把 Hibernate 塞进 Spring 容器管理的桥梁。如果误用了 Hibernate 6 的 SessionFactory 实现,Spring 5.3 根本认不出来,启动时直接报 ClassNotFoundException 或 NoSuchMethodError。所以依赖版本不是随便填,要按这条线锁定:Spring 统一用 5.3.x 系列,SpringSecurity 用 5.8.x 系列,Hibernate 用 5.6.x 系列,连接池可以选 HikariCP 或 DBCP2,JDK 建议 8 或 11。
2.2 pom.xml 依赖清单与关键配置
下面是一个可以直接落地的 pom.xml 核心片段,按“版本属性区、依赖区、编译插件”三段组织。我一般会把所有版本号收敛到 properties 里,避免多个子模块或后续升级时到处改版本。
<properties> <spring.version>5.3.31</spring.version> <springsecurity.version>5.8.10</springsecurity.version> <hibernate.version>5.6.15.Final</hibernate.version> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Spring 基础与 Web MVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> <!-- SpringSecurity 5 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${springsecurity.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${springsecurity.version}</version> </dependency> <!-- Hibernate 5 核心 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <!-- 连接池 --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>4.0.3</version> </dependency> <dependency> <groupId>com.oracle.database.jdbc</groupId> <artifactId>ojdbc8</artifactId> <version>19.8.0.0</version> <scope>runtime</scope> </dependency> </dependencies> <build> <finalName>web-base</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <release>11</release> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build>逻辑说明:spring-tx 提供 @Transactional 注解和事务管理器抽象,spring-orm 提供 Hibernate 集成,SpringSecurity 拆成了 config 和 web 两个包,config 负责注解与 DSL 解析,web 负责过滤器链。war 插件里 failOnMissingWebXml 设为 false,是因为 3.1 节会用 WebApplicationInitializer 走纯 Java 配置,不需要 web.xml。这三个版本属性锁死后,Maven 会用依赖就近原则自行解析传递依赖,如果 Hibernate 传递进来的老版本 servlet-api 与容器冲突,再用 exclusions 排除,这个细节 2.3 节单独说。
2.3 包体形式与依赖冲突的两个先手处理
打包形式我建议直接选 war。这套组合的传统部署路径是外置 Tomcat,war 可以丢进 webapps 目录热部署,也可以配合 Tomcat 的 manager 做远程发布。很多商业项目有专门的运维平台,只认 war 包,不会给 Java 进程单独配启动参数,这时候硬上 SpringBoot jar 反而会引入容器管理上的阻力。
依赖冲突要先手处理三处。第一处是 servlet-api:Hibernate 5.6 依赖的 javax.servlet-api 版本可能旧,Tomcat 9 自带的 servlet-api 是 javax.servlet 4.0,Maven 会把旧版本带进 war 的 WEB-INF/lib,造成 NoSuchMethodError。解决方式是在 Hibernate 依赖里排除 javax.servlet-api,运行时完全交给容器。第二处是 slf4j:Spring 和 Hibernate 都输出日志,但绑定器只能有一个,我通常显式引入 logback-classic,同时把 log4j-to-slf4j 之类的桥接包排除干净。第三处是 jackson:Spring 的 MappingJackson2HttpMessageConverter 需要 jackson-databind,Hibernate 的实体代理又需要 jackson-datatype-hibernate5 来避免懒加载序列化报错,这两个必须版本对齐,否则反序列化时字段丢失。
这三处冲突处理完之后,可以先用mvn clean package -DskipTests打一次包,解压 war 检查 WEB-INF/lib 里是不是只剩一份 servlet-api、一份 slf4j 绑定器。这一步能省掉后面部署时一半以上的玄学问题。
3. Spring 5 与 SpringMVC 5 装配:从 WebApplicationInitializer 到第一个接口
3.1 用 WebApplicationInitializer 替代 web.xml 的启动入口
Spring 5 支持完全去 web.xml 的启动方式,核心入口是实现 WebApplicationInitializer 接口。容器启动时会自动扫描这个实现类,调用 onStartup 方法完成 Spring 根容器和 MVC 子容器的创建。很多老团队从 XML 迁过来不习惯,总觉得没有 web.xml 不放心,实际上这个类做的事情和 web.xml 里配置 ContextLoaderListener 与 DispatcherServlet 完全等价,只是从 XML 描述换成了 Java 代码。
public class WebInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletContext) { // 创建 Spring 根容器,加载 service、dao、security 等公共配置 AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); rootContext.register(RootConfig.class); servletContext.addListener(new ContextLoaderListener(rootContext)); // 创建 SpringMVC 子容器,只扫描 controller 包 AnnotationConfigWebApplicationContext mvcContext = new AnnotationConfigWebApplicationContext(); mvcContext.register(MvcConfig.class); // 注册 DispatcherServlet,接管所有请求 ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(mvcContext)); dispatcher.setLoadOnStartup(1); dispatcher.addMapping("/"); // 字符编码过滤器,必须放在所有过滤器最前面 CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter(); encodingFilter.setEncoding("UTF-8"); encodingFilter.setForceEncoding(true); servletContext.addFilter("characterEncoding", encodingFilter) .addMappingForUrlPatterns(null, false, "/*"); // 委托给 SpringSecurity 的过滤器链 servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy("springSecurityFilterChain")) .addMappingForUrlPatterns(null, false, "/*"); } }这段代码的关键在于父子容器边界。rootContext 注册的是 RootConfig,扫描 service、dao、security 包;mvcContext 注册的是 MvcConfig,只扫描 controller 包。控制器的 @Autowired 会先查子容器再查父容器,所以 service 能被注入进 controller,但 controller 不会被 service 层反向依赖。如果两个容器都扫描了 service 包,会造成重复 Bean 或事务代理失效,这是这类工程最常见的启动翻车点。
3.2 RootConfig 与 MvcConfig 职责拆分
两个配置类的职责要严格分开。RootConfig 负责数据源、SessionFactory、事务管理器、SpringSecurity 相关的 Bean;MvcConfig 只负责控制器、视图解析器、静态资源映射、消息转换器。
@Configuration @ComponentScan(basePackages = {"com.company.project.service", "com.company.project.dao"}) @EnableTransactionManagement public class RootConfig { @Bean public DataSource dataSource() { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:oracle:thin:@//dbhost:1521/orcl"); config.setUsername("app_user"); config.setPassword("encrypted_password_here"); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); return new HikariDataSource(config); } @Bean public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setPackagesToScan("com.company.project.entity"); Properties props = new Properties(); props.setProperty("hibernate.dialect", "org.hibernate.dialect.Oracle12cDialect"); props.setProperty("hibernate.show_sql", "false"); props.setProperty("hibernate.format_sql", "true"); props.setProperty("hibernate.hbm2ddl.auto", "validate"); props.setProperty("hibernate.jdbc.batch_size", "30"); factoryBean.setHibernateProperties(props); return factoryBean; } @Bean public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) { HibernateTransactionManager txManager = new HibernateTransactionManager(); txManager.setSessionFactory(sessionFactory); return txManager; } }这里 sessionFactory 方法的返回类型必须写 LocalSessionFactoryBean 的泛型父类型 SessionFactory,Spring 才能正确暴露 SessionFactory 这个 Bean 给事务管理器。hbm2ddl.auto 在商业系统中建议用 validate 而不是 update,update 在多人协作时会悄悄改表结构,某次上线后字段对不上就麻烦了。dialect 如果用 Oracle 就选 Oracle12cDialect,MySQL 8 对应 MySQL8Dialect,选错会导致分页语句和序列生成策略全部跑偏。
3.3 MvcConfig:视图解析、静态资源与 JSON 序列化
MvcConfig 用 @EnableWebMvc 开启 MVC 注解驱动,然后实现 WebMvcConfigurer 做定制。这里有一个小坑:@EnableWebMvc 一旦开启,SpringBoot 里那种“放个静态页面就能访问”的行为就没了,必须手动配置资源映射。
@Configuration @EnableWebMvc @ComponentScan(basePackages = {"com.company.project.controller"}) public class MvcConfig implements WebMvcConfigurer { @Override public void configureViewResolvers(ViewResolverRegistry registry) { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); registry.viewResolver(resolver); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("/static/"); } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new Hibernate5Module()); converter.setObjectMapper(mapper); converters.add(converter); } }Hibernate5Module 是解决 JSON 序列化懒加载实体时出现 LazyInitializationException 的第一道防线,它会让实体在被序列化时自动过滤未初始化的懒加载属性,而不是直接抛异常。但只靠它治标不治本,事务边界没控制好时该报错还是会报错,这个在第 4 章细说。静态资源路径和 SpringSecurity 的放行规则要同步,否则登录页引用的 css、js 会被安全过滤器拦下来,页面光秃秃一片。
4. Hibernate 5 接入:SessionFactory 参数、事务边界与懒加载两种解法
4.1 SessionFactory 的关键参数与命名策略大坑
Hibernate 5 默认的物理命名策略会把驼峰字段名转成下划线,比如 userId 映射到 user_id。如果你的表结构和实体属性名不一致,启动时用 hbm2ddl.auto=validate 做校验会直接报错。更隐蔽的是,Hibernate 5.6 里如果既不配置物理命名策略也不配置隐式命名策略,某些数据库里生成的 SQL 会把USER_ID和userId混着用,有时候查得到数据有时候查不到,纯属黑匣子问题。
@Bean public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setPackagesToScan("com.company.project.entity"); factoryBean.setPhysicalNamingStrategy(new PhysicalNamingStrategyStandardImpl()); factoryBean.setImplicitNamingStrategy(new ImplicitNamingStrategyJpaCompliantImpl()); factoryBean.setHibernateProperties(hibernateProps()); return factoryBean; }PhysicalNamingStrategyStandardImpl 表示不对实体属性名做任何转换,实体属性叫 userId,表字段也叫 userId,一对一映射。如果你的数据库规范要求全表下划线,那应该实现自己的 PhysicalNamingStrategy,在建表脚本和实体属性之间做一次可控转换,而不是放任 Hibernate 的默认行为。另一组容易被忽略的参数是 hibernate.jdbc.fetch_size 和 hibernate.jdbc.batch_size,fetch_size 默认值偏小,批量查询上千行时会触发多次往返,batch_size 设到 30 左右可以在批量插入时明显减少 SQL 执行次数。
4.2 事务边界:@Transactional 到底该放在哪一层
Hibernate 的 Session 生命周期和事务生命周期必须保持一致,这是用它做商业开发最核心的纪律。事务注解应该放在 Service 层,因为 Service 层才是业务用例的边界;DAO 层只做单表操作,Controller 层只做参数接收和视图路由。如果把 @Transactional 放在 Controller 上,意味着一次 HTTP 请求全程占着一个数据库连接,并发高时连接池很快被耗尽;如果放在 DAO 的每个方法上,一个 Service 方法调用三个 DAO 方法就会开三个事务,中途失败无法回滚。
@Service public class OrderService { @Transactional(rollbackFor = Exception.class) public void createOrder(OrderDTO dto) { Order order = new Order(); order.setUserId(dto.getUserId()); order.setAmount(dto.getAmount()); orderDao.save(order); // 扣减库存,失败时整个事务回滚 stockDao.deduct(dto.getProductId(), dto.getQuantity()); } }rollbackFor = Exception.class 这个参数值得单独强调。Spring 的默认事务回滚策略只回滚 RuntimeException,如果业务方法里抛出的是 IOException 或 SQLException 这类受检异常,事务不会回滚,数据会停留在半完成状态。写商业代码我习惯一律显式声明 rollbackFor 为所有异常,宁可多回滚也不留半截数据。
4.3 懒加载与 LazyInitializationException 的两种解法
Service 层事务结束后,Session 就关了,此时再访问实体的关联集合会抛 LazyInitializationException。常见场景是 Controller 把实体直接丢给视图渲染,JSP 里访问 order.getItems().size() 时炸掉。
解法一是在 Service 层强制初始化,适合对查询性能有明确预期的场景:
public OrderDetailVO getOrderDetail(Long orderId) { Order order = orderDao.findById(orderId); Hibernate.initialize(order.getItems()); // 或者 order.getItems().size(); return convertToVO(order); }解法二是配置 OpenSessionInView,适合老项目快速改造。在 WebInitializer 里注册一个 OpenSessionInViewFilter,把 Session 生命周期拉长到整个 HTTP 请求结束。商业系统要谨慎使用 OSIV,因为长事务意味着连接池压力大、脏读窗口长,很多从 SSH 时代切过来的团队一开始为了省事开 OSIV,后面并发一上来就后悔。
第 3 章里加的 Hibernate5Module 本质是序列化层面的兜底,不建议把它当作唯一解决方案;真正干净的方案是 Service 层把需要展示的数据都查好,实体不直接出接口。调试阶段可以在 DAO 层临时打开 show_sql 和 format_sql,定位 N+1 查询时很有用,但上线一定要关掉,不然业务日志全被 SQL 刷屏。
5. SpringSecurity 5 落地:认证授权配置,附带五条高频排查记录
5.1 HttpSecurity DSL:五段式配置先搭出完整过滤链
SpringSecurity 5 的配置入口是继承 WebSecurityConfigurerAdapter,重写 configure(HttpSecurity http)。配置顺序也很重要:第一段关 CSRF 或配置 CSRF token 来源;第二段放行静态资源和登录页;第三段配置表单登录和登录成功/失败跳转;第四段配置登出行为;第五段对所有剩余请求做认证限制。
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/static/**", "/login", "/logout").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/index", true) .failureUrl("/login?error=true") .permitAll() .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login?logout=true") .invalidateHttpSession(true); } }csrf().disable() 在商业系统中不能盲从。如果系统是前后端分离的接口服务,且无 Cookie 会话之外的防护,建议保留 CSRF 并把 token 放到响应头里;如果是老式 JSP 表单提交,开启 CSRF 后每个表单里要加隐藏域,否则所有 POST 请求都会 403。上例里为了演示主流程先关闭了,实际项目应根据认证方式单独决策。
5.2 UserDetailsService 与密码编码器:登录认证的数据来源
表单登录时 SpringSecurity 需要一个 UserDetailsService 用来加载用户,和一个 PasswordEncoder 用来比对密码。默认的 DaoAuthenticationProvider 会把两者串起来,你只需要提供这两个 Bean 即可。
@Service public class UserDetailsServiceImpl implements UserDetailsService { private final UserDao userDao; public UserDetailsServiceImpl(UserDao userDao) { this.userDao = userDao; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user = userDao.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .authorities(user.getRoles().split(",")) .build(); } }密码编码器建议用 BCryptPasswordEncoder,不要用 MessageDigestPasswordEncoder 或直接明文比对。BCrypt 每次加密结果都带随机盐,同一密码两次生成的哈希不同,但这不影响校验,因为它把盐嵌在哈希串里了。商业系统如果历史数据里是 MD5 加密的,可以用 DelegatingPasswordEncoder 同时支持多种编码方式,让原密码在用户下次登录时平滑升级到 BCrypt,这是老系统改造时最省事的过渡方案。
5.3 方法级安全:@PreAuthorize 与 @EnableGlobalMethodSecurity
SecurityConfig 上加 @EnableGlobalMethodSecurity(prePostEnabled = true) 后,Service 层或 Controller 层就可以用 @PreAuthorize 做细粒度权限控制。这个方法级安全是商业系统的刚需,比如订单查询接口只允许订单所属人本人或管理员访问。
@PreAuthorize("hasRole('ADMIN') or #orderId == authentication.principal.userId") @GetMapping("/orders/{orderId}") public OrderDetailVO getOrder(@PathVariable Long orderId) { return orderService.getOrderDetail(orderId); }注意 hasRole 的语义:如果库里的角色是 ADMIN,需要 hasRole('ADMIN'),SpringSecurity 会自动加上 ROLE_ 前缀去匹配 ROLE_ADMIN,如果用 hasAuthority 则必须写完整权限名。这个细节最容易搞混,写错了会发现自己明明是管理员却一直 403,连日志里都看不出原因。
5.4 常见问题排查:五条高频踩坑记录
以下五条都是这套组合里反复出现的真实问题,按“现象→原因→解决”写清楚,方便你直接对照排查。
现象一:系统部署到 Tomcat 后,POST 表单一提交就返回 403。原因:SpringSecurity 默认开启 CSRF 校验,表单里没有携带 CSRF token。解决:在 JSP 表单里手动添加<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">,或者在配置中为接口按需关闭 CSRF。
现象二:登录页面能打开,但是登录后跳回 login 页面且 URL 带 error。原因:UserDetailsService 里没有加上 BCryptPasswordEncoder Bean,或数据库中密码字段是明文,导致密码比对失败。解决:在 SecurityConfig 中声明 PasswordEncoder Bean,确认数据库密码是 BCrypt 哈希格式。
现象三:接口 500 报 AccessDeniedException,日志被 security 过滤器吃掉看不到堆栈。原因:方法级安全注解没生效,或权限表达式写错。解决:先确认 SecurityConfig 上有 @EnableGlobalMethodSecurity,再把权限表达式简化成 hasRole('ADMIN') 单独验证,逐步排除。
现象四:页面能加载但 css、js 全部 404。原因:静态资源被 SpringSecurity 拦截。解决:在 authorizeRequests 中显式放行静态资源路径,同时确认 MVC 层 addResourceHandlers 配置了对应映射。
现象五:SessionFactory 创建时报 PersistentObjectException 或 PropertyAccessException,提示无法关联到某个实体类。原因:实体类所在的包没有被 LocalSessionFactoryBean 的 setPackagesToScan 扫描到,或者实体类没加 @Entity 注解。解决:核对实体包路径与注解声明,启动时观察 Hibernate 打印的映射实体数量,确认与实体类总数一致。
6. 从跑通到敢上线:日志定位、回归验证与向 SpringBoot 迁移的取舍
6.1 启动慢或者打不开页面时先看哪四项日志
第一项是 Spring 容器启动日志,重点看加载了多少个 Bean,如果数量明显偏少,说明组件扫描路径有问题。第二项是 Hibernate 的映射日志,它会打印每个实体对应的表名和字段映射,在这里能第一时间发现命名策略带来的表名偏差。第三项是 SpringSecurity 的过滤器链初始化日志,它会按顺序打印所有生效的过滤器名称,如果 springSecurityFilterChain 没出现,说明 DelegatingFilterProxy 没注册成功。第四项是数据源初始化日志,HikariCP 会打印连接池启动成功或失败的详细信息,数据库 URL 配错时在这里能定位到具体错误码。
建议在 logback.xml 里临时给 org.springframework、org.hibernate、org.springframework.security 三个包的日志级别设为 DEBUG,启动完成后改回 INFO。生产环境保留 WARN,但要用独立文件滚动策略保存应用启动日志,方便事后追查。
6.2 给接口做一轮事务与安全回归验证
接口写完以后,用 curl 对核心链路做一轮自动化冒烟测试,不依赖前端页面。至少覆盖匿名访问、登录、带权限访问、无权限访问四个场景,确认安全配置在真实 HTTP 请求下行为符合预期。
# 1. 匿名访问受保护资源,预期返回 302 重定向到登录页 curl -I http://localhost:8080/web-base/index # 2. 登录拿 Session,携带凭证访问业务接口 curl -c cookies.txt -d "username=admin&password=test123" \ http://localhost:8080/web-base/login -L # 3. 带会话访问订单详情,预期返回 JSON 数据 curl -b cookies.txt \ -H "Accept: application/json" \ http://localhost:8080/web-base/orders/1001 # 4. 模拟普通用户访问管理员接口,预期返回 403 curl -b cookies.txt -o /dev/null -w "%{http_code}" \ http://localhost:8080/web-base/admin/users事务层的验证要看数据库真实数据。插入一条业务数据后故意让后置操作抛出异常,检查数据库是否回滚;再开一个高并发插入场景,观察连接池是否被打满。这两步在开发环境做完,比上线后靠用户反馈来发现问题要省太多精力。
6.3 向 SpringBoot 迁移时这套骨架能带走什么
如果团队未来决定向 SpringBoot 演进,这套 Maven 骨架里的核心资产不是代码,而是配置结论。RootConfig 里的数据源参数、Hibernate 的命名策略和 batch_size、SpringSecurity 的过滤链顺序、事务回滚策略,这些都可以直接平移到 SpringBoot 的 application.yml 和 @Configuration 类中。SpringBoot 的自动配置会把 LocalSessionFactoryBean 换成 HibernateJpaConfiguration,但 hibernate.properties 里的 dialct、命名策略、批处理参数几乎原样保留;SpringSecurity 的 HttpSecurity DSL 写法完全一致;@PreAuthorize 注解可以不动。
建议现在就把命名策略的标准化、事务边界的约定,还有安全过滤链的放行规则沉淀成团队内部文档。这样真到迁移那一天,风险已经被提前消化掉了。
最后我会用一句实在话收个尾:在商业系统里,稳定不是靠框架新,而是靠配置纪律。这套骨架我至少用它在三个项目里平安上线过,每次有新人问我“怎么让几个 5.x 框架不打架”,答案从来都不在什么神奇的代码片段里,而在于把 pom 的版本、事务的边界、过滤器的顺序这老三样钉死。希望这篇笔记能帮你少走一段弯路。
本文还有配套的精品资源,点击获取