Spring MVC和Spring Boot入门以及Spring Session的详解
Spring MVC
1.spring controller
在我讲述spring的controller前我需要大家先看一个图片,先来了解一下Java web服务都是做的什么事情。如下所示:
其次,spring controller的关键点主要有三个:
- Bean的配置:添加controller注解并运用
- 网络资源的加载:说白了就是加载网页
- 网址路由的配置:RequestMapping注解的运用
1.controller注解的运用。
我在spring的依赖注入文章中就已经提到了,添加了controller注解就是让spring来管理这些bean了。也就是说添加上controller
注解就可以了,如下所示:
import org.springframework.stereotype.Controller;
@Controller
public class HelloControl {
}
2.加载网页
加载网页其实就是返回方法,代码如下:
import org.springframework.stereotype.Controller;
@Controller
public class HelloControl {
//返回的是一个字符串类型
public String say(){
return "hello.html";
}
}
另外,由于spring boot自动帮我们做了加载,因此原本的路径src/main/resources/static/hello.html
,但是我们只需要写static后面的路径就可以了。如果我们在static
包的下面又创建了一个名为html
的包,那么代码应该这样子写:
import org.springframework.stereotype.Controller;
@Controller
public class HelloControl {
public String say(){
//文件路径使用/分割,而且这里的hello.html文件在html包下。
return "html/hello.html";
}
}
3.RequestMapping注解的运用
首先我们要知道一个概念就是路由
,路由说白了就是在web服务器中去解析URL并且将网络资源返回给调用者。而由于sping mvc完美的支持了路由,因此我们只需要添加上RequestMapping
注解并写上参数路径就可以了。
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloControl {
@RequestMapping("/hello")
public String say(){
return "html/hello.html";
}
}
注意:RequestMapping注解相较于GetMapping和PostMapping拥有灵活性,但也同时也有危险性,而后者则明显具有清晰性和安全性。
2.Get Request
我们先来看一下URL的标准格式
我们在本机上访问都是直接输入域名和后面的就可以了,如:127.0.0.1:8080/hello?id=xxx
,这是因为web服务器和浏览器会自动给我们添加上协议,所以完整的就应该是:http://localhost:8080/hello?id=xxx
。但是我们会发现有些URL不需要端口,如https://www.baidu.com/hello?id=xxx
,这是因为https会默认指向端口443,如果没有特殊的端口,一般就默认了。
1.定义参数
我们在spring的代码里面该如何定义参数呢,直接看代码:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
public class SongListControl {
@RequestMapping("/songlist")
public String index( @RequestParam("id") String id){
return "html/songList.html";
}
}
这也就意味着我们访问的URL为https://域名/songList?id=xxxx
,但是如果我们想获取多个参数,那么代码应该这样子写:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
public class SongListControl {
@RequestMapping("/songlist")
//其实这里面的数据类型boolean,String,int是可以自动转换的。
public String index(@RequestParam("id") String id, @RequestParam("pageNum") int pageNum){
return "html/songList.html";
}
}
这样子以来我们需要访问的话URL应该写成:http://xxxx/songlist?id=xxx&pageNum=1
,多个参数之间要用&
分割。
非必需传递参数的写法则如下所示:
@GetMapping("/songlist")
//这里的访问URL我写成/songList?pageNum=xxx&id=xxx和/songList?id=xxx都是一样的。
public String index(@RequestParam(name="pageNum",required = false) int pageNum,@RequestParam("id") String id){
return "html/songList.html";
}
输出JSON数据则添加个ResponseBody注解即可:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
public Class SongListControl{
@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam("id") String id) {
return "ID: " + id;
}
}
我们访问网页后你会发现spring mvc已经自动将JAVA对象转化成json数据了,因此我们一般将这种输入JSON数据的方法称为API
。
我们返回JSON数据其实也可以将controller注解改为RestController注解,因为添加了后者将上面的代码改成了:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@RestController
public Class SongListControl{
@GetMapping("/api/foos")
public String getFoos(@RequestParam("id") String id) {
return "ID: " + id;
}
}
Spring Boot
看过我之前写的spring依赖注入文章的童鞋们应该知道,当时我们启动IOC容器扫描的仅有fm.douban
包下的添加注解的Bean。但是如果里面调用了其他的包下的服务或接口,这样一来就扫描不到了,更无法完成自动的实例化bean了。那么解决方法就是下面的componentScan
了。
1.Spring Boot ComponentScan
1.只要添加了注解springBootApplication的注解就是启动类,我们在里面的参数scanBasePackage输入一个字符串数组,里面就是我们想要扫描的包。
@SpringBootApplication(scanBasePackages={"fm.douban.app", "fm.douban.service"})
public class AppApplication {
public static void main(String[] args) {
SpringApplication.run(AppApplication.class, args);
}
}
2.当然如果不是springboot的启动类,也可以随便创建一个类,并添加上注解componentScan,里面也是输入一个字符串数组,数组里就是我们要扫描的包。
@ComponentScan({"fm.service", "fm.app"})
public class SpringConfiguration {
... ...
}
2.Spring Boot Logger运用
在我之前讲过的spring依赖注入里面我就讲过PostConstruct
注解,它的作用就是在系统启动后这个标记了注解的初始化方法也会自动执行调用。如下:
@PostConstruct
public void init(){
System.out.println("SongListControl 启动啦");
if (subjectService != null) {
System.out.println("subjectService 实例注入成功。");
} else {
System.out.println("subjectService 实例注入失败。");
}
}
但是我们在系统启动后并没有在控制台上看到打印的数据,这是一因为在spring的这种大系统中,System.out.println()
的打印内容在哪里是不确定的,况且,如果在淘宝这样的超大访问量中,光是调试信息都足以把系统撑爆了。因此一般都是用日志来记录。
那么使用日志系统的步骤主要分为2步。
1.配置
即在spring boot
的标准配置文件application.properties
中增加日志级别配置,如:
logging.level.root=info
或者也可以将日志系统配置在不同的包下,即:
logging.level.fm.douban.app=info
另外级别主要有以下四个:
例如我们设置了logging.level.root=info
,那么它就会打印Info
级别以上的日志,即debug
日志并不会打印输出。
2.编码
配置完成后,直接完成日志对象的实例化就可以了,如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
@RestController
public class SongListControl {
//注意这里的static final是为了复用。
//另外在不同类中只需要将getLogger()里面的参数改成对应的类就可以了。
private static final Logger LOG = LoggerFactory.getLogger(SongListControl.class);
@PostConstruct
public void init(){
//里面的内容会打印输出。
LOG.info("SongListControl 启动啦");
}
}
上面只是举例了info()方法,除此之外还有warn(),error(),debug()方法。如下图所示:
3.配置文件Spring Boot Properties
配置文件格式就如上面所说的,在启动系统后,框架会自动加载并解析这些配置文件。当然哈,为了方便阅读,书写配置项时还是尽量遵守下面的规则:
- 相同的前缀的前缀项书写在一起。
- 不同的前缀的前缀项隔开一行写。
配置文件的意义
配置文件就是为了能够在不修改代码的情况下,直接做到不硬编码,解耦。
自定义配置项
我们也可以在配置文件中添加我们自定义配置项,如:
song.name=I Love XiYan
那么在代码中配置自定义配置项则可以
import org.springframework.beans.factory.annotation.Value;
public class SongListControl {
@Value("${song.name}")
private String songName;
//上面代码则就意味着songName变量的值固定为配置项中song.name的值了。
}
另外,在application.properties中配置写多了没有问题,但是一旦写少了,即缺少了某些配置项则会发生报错。
Spring Session
1.Cookie
我们首先要知道什么是cookie,cookie就是指存储在客户端浏览器中的一段文本内容,每条数据都是以key=value
的格式存储。多条数据之间用英文;
隔开。而且目前由于浏览器对cookie的大小和数量都有限制,因此目前的cookie核心功能是用来存储登录数据
。额外也可以存储一些小数据,如是否登录=true,name=李四
。
读取cookie
在spring的框架中,我们可以为control
类的方法增加一个HttpServletRequest
参数,然后通过request.getCookies()
就可以获得网页中的cookie信息了。
package com.example.exercise.control;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
@Controller
public class SongListControl {
@GetMapping("/jinxuan")
@ResponseBody
public Map returnJSON(HttpServletRequest request){
Map returnData = new HashMap<>();
returnData.put("naga","bba");
//request.getCookies()得到的是一个Cookie数组。
Cookie[] cookies = request.getCookies();
returnData.put("cookies",cookies);
return returnData;
}
}
也可以通过注解来读取cookie值,但是这种做法很危险,因为前提我们得知道cookie的名字。代码如下:
import org.springframework.web.bind.annotation.CookieValue;
@RequestMapping("/songlist")
//这里就是将cookie的值赋值给jsessionId,前提是cookie名字一定要填对。
public Map index(@CookieValue("JSESSIONID") String jSessionId) {
Map returnData = new HashMap();
returnData.put("result", "this is song list");
returnData.put("author", songAuthor);
returnData.put("JSESSIONID", jSessionId);
return returnData;
}
上面的代码结果肯定都是错的和空的,因为我们还没写cookie。
写cookie
会读了总得会写吧,那么写cookie的话其实也不难,只是要考虑的因素较多,而且写的操作需要用到HttpServletResponse
类里面的静态方法addCookie(Cookie cookie)
。
package com.example.exercise.control;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
@Controller
public class SongListControl {
@GetMapping("/jinxuan")
@ResponseBody
public Map returnJSON(HttpServletRequest request,HttpServletResponse response){
Map map = new HashMap<>();
//创建cookie对象,里面的参数分别是名称和值。
Cookie cookie = new Cookie("JSESSIONID","session");
//设置cookie会在哪个域名下生成。
cookie.setDomain("127.0.0.1");
//设置cookie的路径,一般就写/,不写其他路径。
cookie.setPath("/");
//设置cookie的最大存活时间,-1就代表浏览器的有效期。
cookie.setMaxAge(-1);
//设置cookie可通过客户端访问。
cookie.setHttpOnly(false);
response.addCookie(cookie);
Cookie[] cookies = request.getCookie();
map.add("cookie",cookies);
return map;
}
}
我们打开浏览器就可以看到如下图所示:
Spring Session API
但是大家想想,要是真的把cookie
里面存储的登录用户信息等就这样明目张胆的放在客户端,非常不安全,因此我们可以使用session
,cookie
其实就是作为session id
的载体(如上图所示)来与客户端通信,废话不多说,直接看代码吧:
package com.example.exercise.control;
import com.example.exercise.model.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
@Controller
public class SongListControl {
@GetMapping("/jinxuan")
@ResponseBody
public Map returnJSON(HttpServletRequest request,HttpServletResponse response){
Map map = new HashMap<>();
User user = new User("User","123456");
//取得HttpSession对象。
HttpSession session = request.getSession();
//往session里面添加信息,即写session。
session.setAttribute("UserMessage",user);
//读取session。
//里面的User类别忘记实现序列化接口。
User user1 = (User) session.getAttribute("UserMessage");
map.put("user",user1);
return map;
}
//以下是一个post请求,其中的user类有name和pwd两种属性。
//里面的RequestBody注解可以将HTTP中请求体的数据自动转换为java对象变得直观可看。
@PostMapping("api/html/xiyan")
@ResponseBody
public User get(@RequestBody User user){
return user;
}
}
结果如下所示:
大家可以看出,这里的有个cookie,但是我们并没有设置cookie呀,我在上面就说过,cookie是作为session Id
的载体来与客户端通信,而里面的内容对于每位用户都是唯一的,之所以是乱码则是进行了加密,即客户端无法像查看cookie一样明确的查看到内容。
另外,cooie是存储在客户端的,一般不能超过4kb,因此存放太多会导致出错。而session是存储在服务端的,存储多少没有限制,但是基于服务端的性能考虑也不能存放太多。
POST
其次,上面还有个返回User
类型的get()
方法,这个方法是POST提交数据,我们直接看图吧。
- 请求行:POST /xiyan HTTP/1.1
后面的是HTTP版本
- 请求头:客户端发送给服务器的信息
- 请求体:即中间写的数据
- 响应头:服务器返回给客户端的信息
Spring Session 配置
既然我们已经知道了系统
是自动将JSESSIONID
放在默认的cookie
中的,但是cookie
作为session Id
的载体,其实它也可以修改属性。
session配置
我们都知道spring工程自带一个配置文件application.properties
,另外我们也可以通过编程式配置。另外,在Spring中它提供了session
配置的依赖,添加了依赖后我们可以直接调用EnableSpringHttpSession
注解来告诉spring来管理session,直接看代码吧:
<!-- spring session 支持 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
</dependency>
package com.example.exercise.config;
//添加了Bean注解的方法返回的对象实例会被转化为Bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import java.util.concurrent.ConcurrentHashMap;
//该注解表明了这个类是一个配置类,spring会自动扫描处理。
@Configuration
//该注解则是告诉spring开始Spring session管理。
@EnableSpringHttpSession
public class SpringHttpSessionConfig {
@Bean
//以下的这个Bean可以序列化和反序列化会话ID的cookie。
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setCookieName("JSESSIONID");
cookieSerializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
cookieSerializer.setCookiePath("/");
//这个方法意味着cookie可以被js脚本访问。
cookieSerializer.setUseHttpOnlyCookie(false);
//这里的最大生命值单位是秒。
cookieSerializer.setCookieMaxAge(24*60*60);
return cookieSerializer;
}
@Bean
//这个注册成Bean则是为了可以将session存储在内存中。
public MapSessionRepository mapSessionRepository(){
//这里调用了ConcurrentHashMap是因为它的线程是安全的,可以多线程共同工作。
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
Spring Request拦截器
创建拦截器
一般我们会先创建一个Interceptor
包,里面存放的就是各样的拦截器类。而且拦截器必须实现HandlerInterceptor
接口,且可以在三个点进行拦截执行。
package com.example.exercise.intercepter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class Interceptor implements HandlerInterceptor {
//下列的preHandle则就是在controller方法执行之前进行拦截,这种通常都是直接跳转登录页。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//这里面没写代码,一般都是可以使用request调用session等。
//必须返回true,才会继续向下执行。
return true;
}
//这种是controller方法执行之后开始拦截,一般用于统计方法执行时间或者日志记录。
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
//下列则为在所有请求都执行完毕之后(thymeleaf渲染完毕),这种不常用,一般都是用来记录整个方法执行时间。
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
实现WebMvcConfigurer
这个是用来管理
拦截器的,所以一般都是放在config包下,创建它很简单,创建一个类实现WebMvcConfigurer
,并调用方法addInterceptor()
,另外别忘记加上@Configuration
注解,让spring自动扫描处理。
package com.example.exercise.config;
import com.example.exercise.intercepter.Interceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfigurerDemo implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//addInterceptor里面参数是我们创建的拦截器,后面方法和里面的参数表示拦截所有URL。
//多个拦截器组成一个拦截器链。
registry.addInterceptor(new Interceptor()).addPathPatterns("/**");
}
}