最近在学习《Spring 实战(第4版)》,结合目前已学习的内容和自己配置项目的经验,整理如下在IDEA中创建Maven WepApp工程,使用Java配置Spring MVC,Spring Data JPA和Spring Security的过程,使项目中拥有基础的Web服务功能、数据库操作和事务管理功能、安全认证和鉴权功能。
这篇博客只关注配置过程,至于各项配置的具体含义,推荐阅读原书和相关文档,
Spring最传统的配置方式是使用XML文件配置方式,这种方式的优点是不侵入代码,修改配置后不需要重新编译;但标签繁多,可读性差,最大的问题是类型不安全,。
Spring还支持注解方式,配置最为简单明了;但侵入代码而且无法配置第三方类库。
而Java配置本身是Java代码,能保证类型安全,而且不侵入代码,也能配置第三方类库。
所以现在推荐使用Java配置为主,注解为辅的方式来配置Spring项目。
web.xml
maven-archetype-webapp默认支持Servlet 2.x,使用老旧的JSP 1.2描述方式,即DTD定义,不支持EL表达式
web.xml
默认的文件内容如下:
<web-app>
为了支持Servlet 3.x,同时使JSP支持EL表达式,将web.xml
文件修改为如下内容:
<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">
打开Project Structure,配置Modules下的Sources,使项目结构如下所示:
配置Modules下的Dependencies,添加Library,选择Tomcat:
新建Run/Debug Configurations,选择新建Tomcat Server -> Local配置,主要配置Deployment如下:
这时Server标签页如下所示:
至此便完成了Maven WepApp项目的初步配置,项目可以正常启动,点击Run,等待IDEA将项目构建并启动,成功之后会自动在浏览器中打开如下页面:
Web项目默认展示的首页是webapp/index.jsp
,在配置Spring MVC后,若想使用Spring Controller指定/
根域名的显示,需要删除这个index.jsp
文件
pom.xml
添加依赖
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webmvcartifactId>
<version>5.1.3.RELEASEversion>
dependency>
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[]{"/"};
}
}
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;
}
}
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的最基本配置,下面介绍最简单的使用方法
edu.websp.controller.Controller
@Controller
@RequestMapping("/")
public class HomeController {
@RequestMapping(method = RequestMethod.GET)
public String home(){
return "home";
}
}
依照ViewResolver的配置,在WEB-INF/jsp/
目录下创建home.jsp
:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Titletitle>
head>
<body>
<h1>Hello! This is My Home Pageh1>
body>
html>
别忘了删除原有的webapp/index.jsp
,否则可能看不到自定义首页。启动项目之后,便可以在浏览器中看到自己的首页了。
在《Spring 实战》中,作者介绍了如何测试Spring MVC的控制器。虽然测试方面我的知识和经验都很少,但我知道这很重要,有必要进行学习
Maven项目在创建之初就自动在pom.xml
中添加了junit
依赖
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.11version>
<scope>testscope>
dependency>
为了创建mock数据,需要导入mockito
<dependency>
<groupId>org.mockitogroupId>
<artifactId>mockito-coreartifactId>
<version>2.23.4version>
<scope>testscope>
dependency>
为了测试Spring MVC的控制器,需要导入spring-test
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>5.1.3.RELEASEversion>
<scope>testscope>
dependency>
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"));
}
}
首先添加junit的启动配置
之后便可以启动测试,不出意外的话应该测试成功
我的目的是项目连接MySQL数据库,所以第一步是先配好MySQL,并创建数据库websp
首先需要导入spring-data-jpa
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-jpaartifactId>
<version>2.1.3.RELEASEversion>
dependency>
接下来导入Hibernate的JPA实现hibernate-jpa
以及Hibernate的核心包hibernate-core
<dependency>
<groupId>org.hibernate.javax.persistencegroupId>
<artifactId>hibernate-jpa-2.1-apiartifactId>
<version>1.0.2.Finalversion>
dependency>
<dependency>
<groupId>org.hibernategroupId>
<artifactId>hibernate-coreartifactId>
<version>5.4.0.Finalversion>
dependency>
为了配置MySQL数据库的数据源,还需要加入c3p0
和mysql-connector-java
<dependency>
<groupId>c3p0groupId>
<artifactId>c3p0artifactId>
<version>0.9.1.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.13version>
dependency>
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
对于Log4j,现在推荐的是Log4j 2,和Log4j有较大不同,配置方法如下
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-slf4j-implartifactId>
<version>2.11.1version>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-webartifactId>
<version>2.11.1version>
dependency>
WEB-INF/
寻找log4j
开头的文件作为其配置文件。自定义配置文件需要在web.xml
文件中添加如下参数,我没能找到在Java中配置该参数的方法: <context-param>
<param-name>log4jConfigurationparam-name>
<param-value>classpath:logging.xmlparam-value>
context-param>
最简单的logging.xml
文件内容如下:
<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>
Log4jServletContainerInitializer
,会被Container自动发现并初始化,会将Log4jServletContextListener
和Log4jServletFilter
添加到ServletContext中,不需要再自己手动配置至此项目中成功集成了Spring Data JPA,可以对数据库进行操作了
在集成了Spring Data JPA后,来完成一个最简单的注册功能。用户在页面输入用户名和密码,后端将其保存到数据库中。
user
表use websp;
create table user(
username varchar(20) primary key,
password varchar(255) not null);
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;
}
}
edu.websp.dao.UserDao
//User实体表类型,String主键类型
public interface UserDao extends JpaRepository<User,String> {
}
继承JpaRepository
接口后,不需要编写实现类。Spring Data能够自动扫描UserDao
接口并创建其实现类进行依赖注入,提供JpaRepository
的18个操作数据库的通用方法
创建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>
在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 和 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 Profiletitle>
head>
<body>
<h1>${msg}h1>
<h2>Username: ${user.username}h2>
<h2>Password: ${user.password}h2>
body>
html>
现在可以打开浏览器,输入localhost:8080/websp/user/register
来测试简单的注册功能了
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-webartifactId>
<version>5.1.2.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-configartifactId>
<version>5.1.2.RELEASEversion>
dependency>
DelegatingFilterProxy
我们需要创建一个扩展了AbstractSecurityWebApplicationInitializer
的类edu.websp.SecurityWebApplicationInitializer
,Spring会发现它,并用它在Web容器中注册DelegatingFilterProxy
。
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
}
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 {
在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
,需要添加依赖
<dependency>
<groupId>org.bouncycastlegroupId>
<artifactId>bcprov-jdk15onartifactId>
<version>1.60version>
dependency>
至此简单的登录和认证鉴权功能就基本完成了