7.3 Web相关配置
7.3.1 Spring Boot提供的自动配置
通过查看WebMvcAutoConfiguration及WebMvcProperties的源码,可以发现Spring Boot为我们提供了如下的自动配置.
1.自动配置的ViewResolver
(1)ContentNegotiatingViewResolver
这是Spring MVC提供的一个特殊的ViewResolver,ContentNegotiatingViewResolver不是自己处理View,而是代理给不同的ViewResolver来处理不同的View,所以它有最高的优先级.
(2)BeanNameViewResolver
在控制器(@Controller)中的一个方法的返回值的字符串(视图名)会根据BeanNameViewResolver去查找Bean的名称为返回字符串的View来渲染视图.是不是不好理解,下面举个小例子
定义BeanNameViewResolver的Bean:
@Bean
public BeanNameViewResolver beanNameViewResolver(){
BeanNameViewResolver resolver = new BeanNameViewResolver();
return resolver;
}
定义一个View的Bean,名称为jsonView:
@Bean
public MappingJackson2JsonView jsonView(){
MappingJackson2JsonView jsonView = new MappingJacksonJsonView();
return jsonView;
}
在控制器中,返回字符串jsonView,它会找Bean的名称为jsonView的视图来渲染:
@RequestMapping(value="/json",produces={MediaType.APPLICATION_JSON_VALUE})
public String json(Model model){
Person single = new Person("aa",11);
model.addAttribute("single",single);
return "jsonView";
}
(3) InternalResourceViewResolver
这个是一个极为常用的ViewResolver,主要通过前缀、后缀,以及控制器中方法来返回视图的字符串,已得到实际页面,Spring Boot的源码如下:
@Bean
@ConditionalOnMissingBean(InternalResourceViewResolver.class)
public InternalResourceViewResolver defaultViewResolver(){
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix(this.prefix);
resolver.setSuffix(this.suffix);
return resolver;
}
2.自动配置的静态资源
在自动配置类的addResourceHandlers方法中定义了以下静态资源的自动配置.
(1)类路径的/static、/public、/resource和/META-INF/resources文件夹下的静态文件直接映射为/,可以通过http://localhost:8080/来访问.
(2)webjar
何谓webjar,webjar就是将我们常用的脚本框架封装在jar包中的jar包,更多关于webjar的内容请访问http://www.webjars.org网站.
把webjar的/META-INF/resources/webjars/下的静态文件映射为/webjar/,可以通过http://localhost:8080/webjar/来访问.
3.自动配置的Formatter和Converter
关于自动配置的Formatter和Converter,我们可以看一下WebMvcAutoConfiguration类中的定义:
7.3.2 接管Spring Boot的Web配置
如果Spring Boot 提供的Spring MVC不符合要求,则可以通过一个配置类(注解有@Configuration的类)加上@EnableWebMvc注解来实现完全自己控制的MVC配置.
当然,通常情况下,Spring Boot的自动配置是符合我们大多数需求的.在你既需要保留Spring Boot提供的便利,又需要增加自己的额外配置的时候,可以定义一个配置类来继承WebMvcConfigurerAdapter,无需使用@EnableWebMvc注解,然后按照第4章讲解的Spring MVC的配置方法来添加Spring Boot为我们所做的其他配置,例如:
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter{
@Override
public void addViewControllers(ViewControllerRegistry registry){
registry.addViewController("/xx").setViewName("/xx");
}
}
值得指出的是,在这里重写的addViewControllers方法,并不会覆盖WebMvcAutoConfiguration中的addViewControllers(在此方法中,Spring Boot将”/”映射至index.html),这也意味着我们自己的配置和Spring Boot的自动配置同时有效,这也是我们推荐添加自己的MVC的配置方式.
7.3.3 注册Servlet、Filter、Listener
当使用嵌入式的Servlet容器(Tomcat、Jetty等)时,我们通过将Servlet、Filter和Listener声明为Spring Bean而达到注册的效果,或者注册ServletRegistrationBean、FilterRegistrationBean和ServletListenerRegistrationBean的Bean.
(1)直接注册Bean的示例,代码如下:
@Bean
public XxServlet xxServlet(){
return new XxServlet();
}
@Bean
public ZzListener zzListener(){
return new ZzListener();
}
(2)通过RegistrationBean示例:
@Bean
public ServletRegistrationBean servletRegistrationBean(){
return new ServletRegistrationBean(new XxServlet(),"/xx/x");
}
@Bean
pulic FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new YyFilter());
registrationBean.setOrder(2);
return regidtrationBean;
}
@Bean
public ServletListenerRegistrationBean<ZzListener> zzListenerServletRegistrationBean(){
return new ServletListenerRegistrationBean<ZzListener>(new ZzListener());
}
7.4 Tomcat配置
本节虽然Tomcat配置,但其实指的是servlet容器的配置,因为Spring Boot默认内嵌的Tomcat为servlet容器,所以本节只讲对Tomcat配置,其实本节的配置对Tomcat、Jetty和Undertow都是通用的.
7.4.1 配置tomcat
关于tomcat的所有属性都在org.springframework.boot.autoconfigure.web.ServerProperties配置类中做了定义,我们只需要application.properties配置属性做配置即可.通用Servlet容器配置都以”server”作为前缀,而Tomcat特有配置都以”server.tomcat”作为前缀,下面举一些常用的例子
server.port=#配置程序端口,默认为8080#
server.session-timeout=#用户会话session过期时间,以秒为单位#
server.context-path=#用户访问路径,默认为/#
配置Tomcat:
server.tomcat.uri-encoding=#配置tomcat编码,默认为UTF-8#
server.tomcat.compression=#Tomcat是否开启压缩,默认关闭off#
更为详细的Servlet容器配置以及Tomcat配置,请查阅官网
7.4.2 代码配置tomcat
如果你需要通过代码的方式配置servlet容器,则可以注册一个实现EmbeddedServeletContainerCustomizer接口的Bean;若想直接配置Tomcat、Jetty、Undertow则可以直接定义TomcatEmbeddedServletContainerFactory、JettyEmbeddedServletContainerFactory、UndertowEmbeddedServletContainerFactory.
1.通用配置
(1)新建类的配置(2.0版本)
public class CustomServletContainer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory factory) {
factory.setPort(9000);
factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,"/404.html"));
}
}
(2) 当前配置文件内配置.若要在当前已有的配置文件内添加类的Bean的话,则在Spring配置中,注意当前类要声明为static:(写在启动类中)
@Component
public static class CustomServletContainer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>{
@Override
public void customize(ConfigurableServletWebServerFactory factory) {
factory.setPort(9000);
factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,"/404.html"));
}
}
2.特定配置
下面以Tomcat为例(Jetty使用JettyEmbeddedServletContainerFactory,Undertow使用UndertowEmbeddedServletContainerFactory):
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
tomcat.addAdditionalTomcatConnectors(createSslConnector()); // 添加http
return tomcat;
}
// 配置https
private Connector createSslConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
try {
File keystore = new ClassPathResource("sample.jks").getFile();
/*File truststore = new ClassPathResource("sample.jks").getFile();*/
connector.setScheme("https");
connector.setSecure(true);
connector.setPort(port);
protocol.setSSLEnabled(true);
protocol.setKeystoreFile(keystore.getAbsolutePath());
protocol.setKeystorePass(key_store_password);
protocol.setKeyPass(key_password);
return connector;
}
catch (IOException ex) {
throw new IllegalStateException("can't access keystore: [" + "keystore"
+ "] or truststore: [" + "keystore" + "]", ex);
}
}
7.4.3 替换tomcat
Spring Boot默认使用Tomcat作为内置Servlet作为内嵌Servlet容器,查看spring-boot-starter-web依赖,如果想要使用Jetty或者Undertow为servlet容器,只需要修改spring-boot-starter-web的依赖即可
1.替换为Jetty
在pom.xml中,将spring-boot-strater-web的依赖由spring-boot-starter-tomcat替换为spring-boot-starter-Jetty
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
7.4.4 SSL配置
SSL的配置也是我们在实际应用中经常遇到的场景
SSL(Secure Sockets Layer 安全套阶层)是为网络通信提供安全及数据完整性的一种安全协议,SSL在网络传输层对网络进行加密.SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通信提供安全支持.SSL协议可分为两层:SSL记录协议,它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装,压缩,加密等基本功能的支持,SSL握手协议,它建立在SSL记录协议之上,用于实际数据传输开始前,通信双方进行身份认证,协商加密算法,交换机密秘钥等
而在基于B/S的Web应用中,是通过HTTPS来实现SSL的,HTTPS是以安全为目标的HTTP通道,简单讲HTTP的安全版,即在HTTP层下加SSL层,HTTPS的安全基础是SSL.
因为Spring Boot用的是内嵌的Tomcat,因而我们做SSL配置的时候需要做如下操作
1.生成证书
使用SSL受限需要一个证书,这个证书可以是自签名的,也可以是从SSL证书授权中心获得的
每一个JDK或者JRE里都有一个工具叫keytool,它是一个证书管理工具,可以用来生成自签名的证书
在配置了JAVA_HOME,并将JAVA_HOME的bin目录加入到Path后,即可在控制台调用该命令
2.spring Boot配置SSL以及http转向到https
7.5 Favicon配置
7.5.1 默认的Favicon
Spring Boot提供了一个默认的Favicon,每次访问应用的时候都能看到
7.5.2 关闭Favicon
我们可以在application.properties中设置关闭Favicon,默认为开启
spring.mvc.favicon.enabled=false
7.5.3 设置自己的Favicon
若需要设置自己的Favicon,则只需要将自己的favicon.ico(文件名不能改动)文件放置在类路径根目录、类路径META-INF/recources/下、类路径resources/下、类路径static/下或类路径public/下.这里将favicon.ico放置在src/main/resources/static下.
7.6 WebSocket
7.6.1 什么是WebSocket
WebSocket为浏览器和服务器提供了双工异步通信的功能,即浏览器可以向服务端发送消息,服务端也可以向浏览器发送消息.WebSocket需要浏览器的支持
WebSocket是通过一个socket来实现双工异步通信能力的.但是直接使用WebSocket(或者SockJS:WebSocket协议的模拟,增加了当浏览器不支持WebSocket的时候的兼容支持)协议开发程序闲的特别繁琐,我们会使用它的子协议STOMP,它是一个更高级别的协议,STOMP协议使用了一个基于帧(frame)的格式来定义消息,与HTTP的request和response类似(具有类似于@RequestMapping和@MessageMapping)
7.6.2 Spring Boot提供的自动配置
Spring Boot对内嵌的Tomcat(7或8)、Jetty9和Undertow使用WebSocket提供了支持.配置源码存于org.springframework.boot.autoconfigure.websocket下
Spring Boot为WebSocket提供了stater pom是spring-boot-starter-websocket.
1.广播式
广播式即服务端有消息时,会将消息给所有连接了当前endpoint(端点)的浏览器.
(1)配置WebSocket,需要在配置类上使用@EnableWebSocketMessageBroker开启WebSocket支持,并通过继承AbstractWeb
@Configuration
@EnableWebSocketMessageBroker//1
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry){//2
registry.addEndpoint("/endpointWisely").withSockJS();//3
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry){//4
registry.enableSimpleBroker("/topic");//5
}
}
代码解释:
①:开启使用STOMP协议来传输基于代理(message broker)的消息,这是控制器支持使用@MessageMapping,就像使用@RequestMapping一样
②:注册STOMP协议的节点(endpoint),并映射到指定的URL
③:注册一个STOMP的endpoint,并指定使用SockJS协议
④:配置消息代理(Message Broker)
⑤:广播式应配置一个/topic消息代理
(2)浏览器向服务端发送消息用此类接受:
public class WiselyMessage {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
(3)服务端向浏览器发送的此类消息:
public class WiselyResponse {
private String responseMessage;
public String getResponseMessage() {
return responseMessage;
}
public WiselyResponse(String responseMessage) {
this.responseMessage = responseMessage;
}
}
(4)演示控制器,代码如下:
@Controller
public class WsController {
@MessageMapping("/welcome")
@SendTo("/topic/getResponse")
public WiselyResponse say(WiselyMessage message) throws Exception{
Thread.sleep(3000);
return new WiselyResponse("Welcome, "+message.getName()+"!");
}
}
代码解释
①:当浏览器向服务端发送请求时,通过@MessageMapping映射/welcome这个地址,类似于@RequestMapping
②:当服务端有消息时,会对订阅了@SendTo中的路径的浏览器发送消息
(5)添加脚本.将stomp.min.js(STOMP协议的客户端脚本)、sockjs.min.js(SockJS的客户端脚本)以及jQuery放置在src/mainmresources/static下
(6)演示页面.在src/main/resources/templates下新建ws.html,代码如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Spring Boot+WebSocket+广播式</title>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">貌似你的浏览器不支持websocket</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">连接</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button>
</div>
<div id="conversationDiv">
<label>输入你的名字</label><input type="text" id="name" />
<button id="sendName" onclick="sendName();">发送</button>
<p id="response"></p>
</div>
</div>
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected) {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
$('#response').html();
}
function connect() {
var socket = new SockJS('/endpointWisely'); //1
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/getResponse', function(respnose){ //2
showResponse(JSON.parse(respnose.body).responseMessage);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName() {
var name = $('#name').val();
//3
stompClient.send("/welcome", {}, JSON.stringify({ 'name': name }));
}
function showResponse(message) {
var response = $("#response");
response.html(message);
}
</script>
</body>
</html>
代码解释:
①:连接SockJS的endpoint名称为”/endpointWisely”
②:使用STOMP子协议的WebSocket客户端
③:连接WebSocket服务端
④:通过stompClient.subscribe订阅/topic/getResponse目标(destination)发送的消息,这个是在控制器的@SendTo中定义的
⑤:通过stompClient.send向/welcome目标(destination)发送消息,这个是在控制器的@MessageMapping中定义的
(7)配置viewController,为ws.html提供便捷的映射路径:
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry){
registry.addViewController("/ws").setViewName("/ws");
}
}
(8)运行,我们的预期效果是:当一个浏览器发送一个消息到服务端时,其他浏览器也能接收到从服务端发送来的这个消息.
开启三个浏览器窗口,并访问http://localhost:8080/ws 分别连接服务器.然后一个浏览器发送一条消息
2.点对点式
广播式有自己的应用场景,但是广播式不能解决我们一个常见的场景,即消息由谁发送、由谁接收的问题.
本例中演示一个简单的聊天程序.例子中有两个用户,互相发送消息给彼此,因需要用户相关的内容,所以先在这里引入最简单的Spring Security相关内容.
(1)添加Spring Security的starter pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
(2) Spring Security的简单配置.这里不对Spring Security做过多的解释,值解释对本项目由帮助的部分:
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
}
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/login").permitAll()//1
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")//2
.defaultSuccessUrl("/chat")//3
.permitAll()
.and()
.logout()
.permitAll();
}
//4
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder())
.withUser("wyf").password("wyf").roles("USER")
.and()
.withUser("wisely").password("wisely").roles("USER");
}
@Override
public void configure(WebSecurity web){
web.ignoring().antMatchers("/resources/static/**");
}
}
代码解释:
①:设置Spring Security 对/和/login路径不拦截
②:设置Spring Security的登陆页面访问的路径为/login
③:登陆成功后转向/chat路径
④:在内存中分别配置两个用户wyf和wisely,密码和用户名一致,角色是USER.
⑤:/resource/static/目录下的静态资源,Spring Security不拦截
(3)配置WebSocket:
@Configuration
@EnableWebSocketMessageBroker//1
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry){
registry.addEndpoint("/endpointWisely").withSockJS();
registry.addEndpoint("/endpointChat").withSockJS();//1
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry){
registry.enableSimpleBroker("/queue","/topic");//2
}
}
代码解释:
①:注册一个名为/endpointChat的endpoint.
②:点对点应增加一个/queue消息代理
(4)控制器.在WWScontroller内添加如下代码
@Controller
public class WsController {
@Autowired
private SimpMessagingTemplate messagingTemplate;//1
@MessageMapping("/welcome")
@SendTo("/topic/getResponse")
public WiselyResponse say(WiselyMessage message) throws Exception{
Thread.sleep(3000);
return new WiselyResponse("Welcome, "+message.getName()+"!");
}
@MessageMapping("/chat")
public void handleChat(Principal principal,String msg){//2
if(principal.getName().equals("wsf")){//3
messagingTemplate.convertAndSendToUser("wisely","/queue/notifications",principal.getName()+"-send:" +msg);//4
}else{
messagingTemplate.convertAndSendToUser("wsy","/queue/notifications",principal.getName()+"-send:" +msg);
}
}
}
代码解释:
①:通过SimpMessagingTemplate 向浏览器发送消息
②:在Spring MVC中,可以直接在参数中获得principal,principle中包含当前用户的信息.
③:这里是一段硬编码,如果发送人是wyf,则发送给wisely;如果发送人是wisely,则发送给wyf,读者可以根据项目实际需要改此处代码.
④:通过messagingTemplate.convertAndSendToUser向用户发送消息,第一个参数是接收消息的用户,第二个是浏览器订阅的地址,第三个是消息本身.
(5)登录页面.在src/main/resources/templates下新建login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<meta charset="UTF-8" />
<head>
<title>登陆页面</title>
</head>
<body>
<div th:if="${param.error}">
无效的账号和密码
</div>
<div th:if="${param.logout}">
你已注销
</div>
<form th:action="@{/login}" method="post">
<div><label> 账号 : <input type="text" name="username"/> </label></div>
<div><label> 密码: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="登陆"/></div>
</form>
</body>
</html>
(6) 聊天页面.在src/main/resources/templates下新建chat.html,代码如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8" />
<head>
<title>Home</title>
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>
</head>
<body>
<p>
聊天室
</p>
<form id="wiselyForm">
<textarea rows="4" cols="60" name="text"></textarea>
<input type="submit"/>
</form>
<script th:inline="javascript">
$('#wiselyForm').submit(function(e){
e.preventDefault();
var text = $('#wiselyForm').find('textarea[name="text"]').val();
sendSpittle(text);
});
var sock = new SockJS("/endpointChat"); //1
var stomp = Stomp.over(sock);
stomp.connect('guest', 'guest', function(frame) {
stomp.subscribe("/user/queue/notifications", handleNotification);//2
});
function handleNotification(message) {
$('#output').append("<b>Received: " + message.body + "</b><br/>")
}
function sendSpittle(text) {
stomp.send("/chat", {}, text);//3
}
$('#stop').click(function() {sock.close()});
</script>
<div id="output"></div>
</body>
</html>
代码解释
①:连接endpoint名称为”/endpointChat”的端点
②:订阅/user/queue/notifications发送消息,这里与在控制器的messagingTemplate.convertAndSendToUser中定义的订阅地址保持一致.这里多了一个/user,并且这个/user是必须的,使用了/user才会发送消息给指定的用户.
(7)增加页面的viewController
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry){
registry.addViewController("/ws").setViewName("/ws");
registry.addViewController("/login").setViewName("/login");
registry.addViewController("/chat").setViewName("/chat");
}
}
(8)运行.我们预期的效果是:两个用户登录系统,可以互发消息.但是在一个浏览器的用户会话session是共享的,我们可以在谷歌浏览器设置两个独立的用户,从而实现用户会话session隔离.