使用Java配置Spring MVC,Spring Data JPA,Spring Security
最近在学习《Spring 实战(第4版)》,结合目前已学习的内容和自己配置项目的经验,整理如下在IDEA中创建Maven WepApp工程,使用Java配置Spring MVC,Spring Data JPA和Spring Security的过程,使项目中拥有基础的Web服务功能、数据库操作和事务管理功能、安全认证和鉴权功能。
这篇博客只关注配置过程,至于各项配置的具体含义,推荐阅读原书和相关文档,
项目基本开发环境
- Oracle JDK 1.8
- IDEA 183
- Tomcat 8.5
- Maven 3.6
项目中使用的主要框架及版本
- Spring MVC 5.1.3
- Spring Test 5.1.3
- Spring Data JPA 2.1.3
- Spring Security 5.1.2
- Apache Log4j 2.11.1
- Hibernate 5.4.0
- Hibernate JPA 2.1 1.0.2
- 其他相关的包见下面的配置过程
项目使用Java配置的主要原因
Spring最传统的配置方式是使用XML文件配置方式,这种方式的优点是不侵入代码,修改配置后不需要重新编译;但标签繁多,可读性差,最大的问题是类型不安全,。
Spring还支持注解方式,配置最为简单明了;但侵入代码而且无法配置第三方类库。
而Java配置本身是Java代码,能保证类型安全,而且不侵入代码,也能配置第三方类库。
所以现在推荐使用Java配置为主,注解为辅的方式来配置Spring项目。
新建Maven工程
1 在IDEA中选择新建Maven WebApp项目
2 完成新建向导
3. 修改web.xml
maven-archetype-webapp默认支持Servlet 2.x,使用老旧的JSP 1.2描述方式,即DTD定义,不支持EL表达式
web.xml
默认的文件内容如下:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
为了支持Servlet 3.x,同时使JSP支持EL表达式,将web.xml
文件修改为如下内容:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"
metadata-complete="true">
4. 配置项目结构
打开Project Structure,配置Modules下的Sources,使项目结构如下所示:
5 添加Tomcat依赖库
配置Modules下的Dependencies,添加Library,选择Tomcat:
6 确认Web Facets
IDEA自动配置:
7 确认 Artifacts
IDEA自动配置:
启动项目
1 添加启动配置
新建Run/Debug Configurations,选择新建Tomcat Server -> Local配置,主要配置Deployment如下:
这时Server标签页如下所示:
2 启动项目
至此便完成了Maven WepApp项目的初步配置,项目可以正常启动,点击Run,等待IDEA将项目构建并启动,成功之后会自动在浏览器中打开如下页面:
Web项目默认展示的首页是webapp/index.jsp
,在配置Spring MVC后,若想使用Spring Controller指定/
根域名的显示,需要删除这个index.jsp
文件
配置Spring MVC
1 向pom.xml
添加依赖
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
2 创建edu.websp.config.WebApplicationInitializer
一般应该是将
WebConfig.class
配置到Servlet Context
中,但是Spring Security的配置类需要和Spring MVC的配置类处在同一个Context中,详情可参见官方文档 ,否则配置好Spring Security后启动项目会报错:
Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext
所以我采取了将所有配置放在Root Config中的办法,MVC和Security的配置类都导入到RootConfig.class
中
public class WebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
3 创建edu.websp.config.WebConfig
配置类
@Configuration
@ComponentScan(value = {"edu.websp.controller"})
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver(){
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/jsp/");
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
}
4 创建edu.websp.config.RootConfig
配置类
@Configuration
@Import(value = {WebConfig.class})
@ComponentScan(basePackages = {"edu.websp"},
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.ANNOTATION,value = EnableWebMvc.class)
})
public class RootConfig {
}
以上便基本完成了Spring MVC的最基本配置,下面介绍最简单的使用方法
Spring MVC 简单使用
1 编写基本的edu.websp.controller.Controller
@Controller
@RequestMapping("/")
public class HomeController {
@RequestMapping(method = RequestMethod.GET)
public String home(){
return "home";
}
}
2 编写基本JSP
依照ViewResolver的配置,在WEB-INF/jsp/
目录下创建home.jsp
:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Hello! This is My Home Page</h1>
</body>
</html>
别忘了删除原有的webapp/index.jsp
,否则可能看不到自定义首页。启动项目之后,便可以在浏览器中看到自己的首页了。
使用Spring Test测试
在《Spring 实战》中,作者介绍了如何测试Spring MVC的控制器。虽然测试方面我的知识和经验都很少,但我知道这很重要,有必要进行学习
1 添加Spring MVC测试相关依赖包
Maven项目在创建之初就自动在pom.xml
中添加了junit
依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
为了创建mock数据,需要导入mockito
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
为了测试Spring MVC的控制器,需要导入spring-test
<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.3.RELEASE</version>
<scope>test</scope>
</dependency>
2 编写edu.websp.test.HomeController
的测试类
public class HomeControllerTest {
@Test
public void homeTest() throws Exception{
HomeController homeController = new HomeController();
MockMvc mockMvc = MockMvcBuilders.
standaloneSetup(homeController).build();
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.view().name("home"));
}
}
3 启动测试
首先添加junit的启动配置
之后便可以启动测试,不出意外的话应该测试成功
集成Spring Data JPA
1 创建数据库
我的目的是项目连接MySQL数据库,所以第一步是先配好MySQL,并创建数据库websp
2 导入依赖
首先需要导入spring-data-jpa
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-jpa -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
接下来导入Hibernate的JPA实现hibernate-jpa
以及Hibernate的核心包hibernate-core
<!-- https://mvnrepository.com/artifact/org.hibernate.javax.persistence/hibernate-jpa-2.1-api -->
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.1-api</artifactId>
<version>1.0.2.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-core -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.0.Final</version>
</dependency>
为了配置MySQL数据库的数据源,还需要加入c3p0
和mysql-connector-java
<!-- https://mvnrepository.com/artifact/c3p0/c3p0 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
3 编写Spring Date JPA的配置类edu.websp.config.DataJPAConfig
配置类各部分说明见代码注释
@Configuration
//启用Repository接口扫描,Spring Data JPA关键
@EnableJpaRepositories("edu.websp.dao")
//启用事务管理
@EnableTransactionManagement
public class DataJpaConfig {
/**
* 配置数据源
* @return
*/
@Bean
public ComboPooledDataSource dataSource(){
ComboPooledDataSource dataSource = new ComboPooledDataSource();
try{
dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
}catch (Exception e){
}
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/websp");
dataSource.setUser("root");
dataSource.setPassword("root");
dataSource.setInitialPoolSize(5);
dataSource.setMaxPoolSize(10);
return dataSource;
}
/**
* 配置EntityManagerFactory
* @return
*/
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(false);
vendorAdapter.setDatabase(Database.MYSQL);
vendorAdapter.setShowSql(true);
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("edu.websp.entity");
factory.setDataSource(dataSource());
return factory;
}
/**
* 配置TransactionManager
* @param entityManagerFactory
* @return
*/
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
}
将DataJpaConfig.class
导入到RootConfig
中
@Import(value = {WebConfig.class,DataJpaConfig.class})
public class RootConfig {
}
至此配置基本完成,但如果这时直接启动项目的话会出现如下信息
Failed to load class "org.slf4j.impl.StaticLoggerBinder".
Spring Data JPA 使用了日志功能,我的解决办法是配置Log4j
4 配置Log4j2
对于Log4j,现在推荐的是Log4j 2,和Log4j有较大不同,配置方法如下
- 导入如下依赖
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.11.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-web --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-web</artifactId> <version>2.11.1</version> </dependency>
- 指定Log4j 2的配置文件
Log4j 2默认情况下会在WEB-INF/
寻找log4j
开头的文件作为其配置文件。自定义配置文件需要在web.xml
文件中添加如下参数,我没能找到在Java中配置该参数的方法:
最简单的<context-param> <param-name>log4jConfiguration</param-name> <param-value>classpath:logging.xml</param-value> </context-param>
logging.xml
文件内容如下:<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> </Appenders> <Loggers> <Root level="error"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>
- 自动配置
对于Servlet 3及以上的Web应用,Log4j 2 Web JAR文件提供了Log4jServletContainerInitializer
,会被Container自动发现并初始化,会将Log4jServletContextListener
和Log4jServletFilter
添加到ServletContext中,不需要再自己手动配置
至此项目中成功集成了Spring Data JPA,可以对数据库进行操作了
简单的注册功能
在集成了Spring Data JPA后,来完成一个最简单的注册功能。用户在页面输入用户名和密码,后端将其保存到数据库中。
1 新建user
表
use websp;
create table user(
username varchar(20) primary key,
password varchar(255) not null);
2 新建edu.websp.entity.User
实体类
@Entity
@Table(name="user")
public class User {
private String username;
private String password;
@Id
@Column(name = "username")
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Column(name = "password")
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
3 编写Repository接口edu.websp.dao.UserDao
//User实体表类型,String主键类型
public interface UserDao extends JpaRepository<User,String> {
}
继承JpaRepository
接口后,不需要编写实现类。Spring Data能够自动扫描UserDao
接口并创建其实现类进行依赖注入,提供JpaRepository
的18个操作数据库的通用方法
4 显示注册页面
创建edu.websp.controller.UserConroller
控制器,添加对UserDao
的依赖,并添加对GET /user/register
的处理方法
@Controller
@RequestMapping("/user")
public class UserController {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@RequestMapping(value = "/register",method = RequestMethod.GET)
public String registerPage(){
return "register";
}
}
创建WEB-INF/jsp/register.jsp
,使用表单,输入用户名和密码,点击提交默认发起POST /user/register
注册请求
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>注册</title>
</head>
<body>
<form method="POST" enctype="multipart/form-data">
Username: <input type="text" name="username"/><br/>
Password: <input type="text" name="password"/><br/>
<input type="submit" value="Register"/>
</form>
</body>
</html>
5 处理注册请求
在userController
中添加处理注册请求的方法,判断用户名是否已存在,若不存在则将其保存到数据库中,并重定向到/usr/{username}
显示用户信息,注意这里使用了Spring的flash
属性跨重定向请求传递信息参数。
@RequestMapping(value = "/register",method = RequestMethod.POST)
public String processRegister(User user, RedirectAttributes model){
if(!userDao.existsById(user.getUsername())){
userDao.save(user);
model.addFlashAttribute("user",user);
model.addFlashAttribute("msg","新建用户");
}else{
//注意此处使用Optional<User> 和 dbUser.get()来获取User,只有这样才不报错
//直接使用getOne()会报错
Optional<User> dbUser = userDao.findById(user.getUsername());
model.addFlashAttribute("user",dbUser.get());
model.addFlashAttribute("msg","用户已存在");
}
model.addAttribute("username",user.getUsername());
return "redirect:/user/{username}";
}
这里要注意的是如果使用
userDao.getOne()
,会报异常:
Method threw 'org.hibernate.LazyInitializationException' exception. Cannot evaluate edu.websp.entity.User$HibernateProxy$XdqO0mcm.toString()
这是因为对userDao.getOne()
的调用不在一个事务中。userDao.getOne()
使用懒加载机制,获得的是一个User
的代理类引用,依赖于事务和持久化上下文,当持久化上下文关闭后,调用User的任何方法都会报LazyInitializationException
。参考stack overflow
添加处理重定向url的方法,判断model中是否包含传递参数,如果不存在则从数据库中读取并显示信息,否则显示model中的信息。
@RequestMapping(value = "/{username}",method = RequestMethod.GET)
public String info(@PathVariable(name = "username") String username,
Model model){
if(!model.containsAttribute("user")){
if(userDao.existsById(username)){
model.addAttribute("user",userDao.findById(username).get());
}else{
model.addAttribute("user",null);
}
model.addAttribute("msg","从数据库读取");
}
return "info";
}
创建/WEB-INF/jsp/info.jsp
,显示重定向页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Your Profile</title>
</head>
<body>
<h1>${msg}</h1>
<h2>Username: ${user.username}</h2>
<h2>Password: ${user.password}</h2>
</body>
</html>
现在可以打开浏览器,输入localhost:8080/websp/user/register
来测试简单的注册功能了
集成Spring Security
1 导入依赖
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-web -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.1.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-config -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.1.2.RELEASE</version>
</dependency>
2 Java注册DelegatingFilterProxy
我们需要创建一个扩展了AbstractSecurityWebApplicationInitializer
的类edu.websp.SecurityWebApplicationInitializer
,Spring会发现它,并用它在Web容器中注册DelegatingFilterProxy
。
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
}
3 编写注册类edu.websp.WebSecurityConfig
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.mvcMatchers("/user/register").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and().httpBasic();
}
}
启用Spring Security需要在配置类加@EnableWebSecurity
注解,配置Spring Security最简单的方法就是扩展WebSecurityConfigurerAdapter
,这会将应用严格锁定,我们可以通过重写三个configure
方法来配置自定义的Web安全性。
上面重写了void configure(HttpSecurity)
方法,通过拦截器保护请求,上述代码的含义是
.cors()
,允许跨域.csrf().disable()
,禁用csrf
功能(只是简单起见,开发中应该启用).mvcMatchers("/spitter/register").permitAll()
,允许无条件访问登录页面和接口.anyRequest().authenticated()
,其它任何请求都需要认证.formLogin().permitAll()
,默认跳转到登录页面.httpBasic()
,启用HTTP Basic认证
将WebSecurityConfig
导入到RootConfig
中去
@Import(value = {WebConfig.class,DataJpaConfig.class,WebSecurityConfig.class})
public class RootConfig {
4 提供简单的用户认证和鉴权功能
在WebSecurityConfig
中重写void configure(AuthenticationManagerBuilder)
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.mvcMatchers("/user/register").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and().httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username,password,true"+
" from user where username=?")
.authoritiesByUsernameQuery(
"select username,'ROLE_USER' from user where username=?"
).passwordEncoder(new SCryptPasswordEncoder());
}
}
上述代码主要就是设置通过数据库认证,配置数据源,并重写了默认的用户查询功能,通过查询我们自己的user表来获取相关信息。
这里需要注意的一点是这里通过passwordEncoder
指定了认证时使用密码转码器,将登录提交的密码加密后再与数据库中的加密密码比较。为此,我们在处理注册方法中将密码用同一密码转码器加密,再保存到数据库中
@RequestMapping(value = "/register",method = RequestMethod.POST)
public String processRegister(User user, RedirectAttributes model){
if(!userDao.existsById(user.getUsername())){
//密码加密
user.setPassword(new SCryptPasswordEncoder().encode(user.getPassword()));
userDao.save(user);
model.addFlashAttribute("user",user);
model.addFlashAttribute("msg","新建用户");
}else{
Optional<User> dbUser = userDao.findById(user.getUsername());
model.addFlashAttribute("user",dbUser.get());
model.addFlashAttribute("msg","用户已存在");
}
model.addAttribute("username",user.getUsername());
return "redirect:/user/{username}";
}
我们使用了SCryptPasswordEncoder
,需要添加依赖
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.60</version>
</dependency>
至此简单的登录和认证鉴权功能就基本完成了