[Spring实战学习] 使用Java配置Spring MVC,Spring Data JPA,Spring Security

使用Java配置Spring MVC,Spring Data JPA,Spring Security

最近在学习《Spring 实战(第4版)》,结合目前已学习的内容和自己配置项目的经验,整理如下在IDEA中创建Maven WepApp工程,使用Java配置Spring MVC,Spring Data JPA和Spring Security的过程,使项目中拥有基础的Web服务功能、数据库操作和事务管理功能、安全认证和鉴权功能。

这篇博客只关注配置过程,至于各项配置的具体含义,推荐阅读原书和相关文档,

项目基本开发环境

  1. Oracle JDK 1.8
  2. IDEA 183
  3. Tomcat 8.5
  4. Maven 3.6

项目中使用的主要框架及版本

  1. Spring MVC 5.1.3
  2. Spring Test 5.1.3
  3. Spring Data JPA 2.1.3
  4. Spring Security 5.1.2
  5. Apache Log4j 2.11.1
  6. Hibernate 5.4.0
  7. Hibernate JPA 2.1 1.0.2
  8. 其他相关的包见下面的配置过程

项目使用Java配置的主要原因

Spring最传统的配置方式是使用XML文件配置方式,这种方式的优点是不侵入代码,修改配置后不需要重新编译;但标签繁多,可读性差,最大的问题是类型不安全,。

Spring还支持注解方式,配置最为简单明了;但侵入代码而且无法配置第三方类库。

而Java配置本身是Java代码,能保证类型安全,而且不侵入代码,也能配置第三方类库。

所以现在推荐使用Java配置为主,注解为辅的方式来配置Spring项目。

新建Maven工程

1 在IDEA中选择新建Maven WebApp项目

[Spring实战学习] 使用Java配置Spring MVC,Spring Data JPA,Spring Security_第1张图片

2 完成新建向导

[Spring实战学习] 使用Java配置Spring MVC,Spring Data JPA,Spring Security_第2张图片

3. 修改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">

4. 配置项目结构

打开Project Structure,配置Modules下的Sources,使项目结构如下所示:
[Spring实战学习] 使用Java配置Spring MVC,Spring Data JPA,Spring Security_第3张图片

5 添加Tomcat依赖库

配置Modules下的Dependencies,添加Library,选择Tomcat:
[Spring实战学习] 使用Java配置Spring MVC,Spring Data JPA,Spring Security_第4张图片

6 确认Web Facets

IDEA自动配置:
[Spring实战学习] 使用Java配置Spring MVC,Spring Data JPA,Spring Security_第5张图片

7 确认 Artifacts

IDEA自动配置:
[Spring实战学习] 使用Java配置Spring MVC,Spring Data JPA,Spring Security_第6张图片

启动项目

1 添加启动配置

新建Run/Debug Configurations,选择新建Tomcat Server -> Local配置,主要配置Deployment如下:
[Spring实战学习] 使用Java配置Spring MVC,Spring Data JPA,Spring Security_第7张图片
这时Server标签页如下所示:
[Spring实战学习] 使用Java配置Spring MVC,Spring Data JPA,Spring Security_第8张图片

2 启动项目

至此便完成了Maven WepApp项目的初步配置,项目可以正常启动,点击Run,等待IDEA将项目构建并启动,成功之后会自动在浏览器中打开如下页面:
[Spring实战学习] 使用Java配置Spring MVC,Spring Data JPA,Spring Security_第9张图片
Web项目默认展示的首页是webapp/index.jsp,在配置Spring MVC后,若想使用Spring Controller指定/根域名的显示,需要删除这个index.jsp文件

配置Spring MVC

1 向pom.xml添加依赖

    
    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-webmvcartifactId>
      <version>5.1.3.RELEASEversion>
    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>Titletitle>
head>
<body>
    <h1>Hello! This is My Home Pageh1>
body>
html>

别忘了删除原有的webapp/index.jsp,否则可能看不到自定义首页。启动项目之后,便可以在浏览器中看到自己的首页了。

使用Spring Test测试

在《Spring 实战》中,作者介绍了如何测试Spring MVC的控制器。虽然测试方面我的知识和经验都很少,但我知道这很重要,有必要进行学习

1 添加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>

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实战学习] 使用Java配置Spring MVC,Spring Data JPA,Spring Security_第10张图片
之后便可以启动测试,不出意外的话应该测试成功

集成Spring Data JPA

1 创建数据库

我的目的是项目连接MySQL数据库,所以第一步是先配好MySQL,并创建数据库websp

2 导入依赖

首先需要导入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数据库的数据源,还需要加入c3p0mysql-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>

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有较大不同,配置方法如下

  1. 导入如下依赖
    	
        <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>
    
  2. 指定Log4j 2的配置文件
    Log4j 2默认情况下会在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>
    
  3. 自动配置
    对于Servlet 3及以上的Web应用,Log4j 2 Web JAR文件提供了Log4jServletContainerInitializer,会被Container自动发现并初始化,会将Log4jServletContextListenerLog4jServletFilter添加到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 和 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来测试简单的注册功能了

集成Spring Security

1 导入依赖

    
    <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>

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,需要添加依赖

    
    <dependency>
      <groupId>org.bouncycastlegroupId>
      <artifactId>bcprov-jdk15onartifactId>
      <version>1.60version>
    dependency>

至此简单的登录和认证鉴权功能就基本完成了

你可能感兴趣的:(java基础,Web)