03 SpringBoot对Web的支持

支持

SpringBoot提供了

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

为我们提供了嵌入式的Tomcat以及SpringMVC的依赖

和Web相关的自动配置存储在spring-boot-autoconfigure-1.5.3.RELEASE.jar的org.springframework.boot.autoconfigure.web下

开发过程中有什么配置有疑惑了,就去找个包下找相关的自动配置类进行查看即可

Thymealeaf模板引擎

Spring Boot中推荐使用Thymeleaf作为模板引擎

内嵌的Tomcat、Jetty是不支持以jar的形式运行 JSP 的

Ubdertow不支持jsp

Thymealeaf是一个Java类库,是一个xml、xhtml、html5的模板引擎,可以作为Web应用的View层

配置Thymeleaf

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
<groupId>org.zln.learning.spboot</groupId>
	<artifactId>spboot-demo02</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
<name>spboot-demo02</name>
	<description>Demo project for Spring Boot</description>
<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.3.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>
<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

当前导入的Thymealeaf版本仍旧是2,

如果想要使用Thymealeaf3,就需要自己配置

<properties>
    <thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
    <thymeleaf-layout-dialect.version>2.0.4</thymeleaf-layout-dialect.version>
</properties>

仅添加这两个属性配置即可,starter仍旧用的是thymeleaf

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring4.view.ThymeleafViewResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;
import org.thymeleaf.templatemode.*;
@Configuration
public class ThymeleafConfig implements ApplicationContextAware{
    private static final String UTF8 = "UTF-8";
    private ApplicationContext applicationContext;
private String[] array(String ...args) {
        return args;
    }
@Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
private TemplateEngine templateEngine(ITemplateResolver templateResolver) {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(templateResolver);
        return engine;
    }
private ITemplateResolver htmlTemplateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(applicationContext);
        resolver.setPrefix("classpath:/templates/");
        resolver.setTemplateMode(TemplateMode.HTML);
        return resolver;
    }
@Bean
    public ViewResolver htmlViewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine(htmlTemplateResolver()));
        resolver.setContentType("text/html");
        resolver.setCharacterEncoding(UTF8);
        resolver.setViewNames(array("*.html"));
        resolver.setCache(false);
        return resolver;
    }
private ITemplateResolver cssTemplateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(applicationContext);
        resolver.setPrefix("classpath:/templates/");
        resolver.setTemplateMode(TemplateMode.CSS);
        return resolver;
    }
@Bean
    public ViewResolver cssViewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine(cssTemplateResolver()));
        resolver.setContentType("text/css");
        resolver.setCharacterEncoding(UTF8);
        resolver.setViewNames(array("*.css"));
        resolver.setCache(false);
        return resolver;
    }
private ITemplateResolver javascriptTemplateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(applicationContext);
        resolver.setPrefix("classpath:/templates/");//Thymeleaf的HTML文件放在此
        resolver.setTemplateMode(TemplateMode.JAVASCRIPT);
        return resolver;
    }
@Bean
    public ViewResolver javascriptViewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine(javascriptTemplateResolver()));
        resolver.setContentType("application/javascript");
        resolver.setCharacterEncoding(UTF8);
        resolver.setViewNames(array("*.js"));
        resolver.setCache(false);
        return resolver;
    }
}

  • Thymealeaf默认配置

见:ThymeleafProperties 类

首页

默认静态首页

  • classpath:/META-INF/resources/index.html
  • classpath:/resources/index.html
  • classpath:/static/index.html
  • classpath:/public/index.html

小结

如果不想要使用Thymealeaf作为View层,仍旧使用JSP的话,

那么创建好Spring Boot 的 Web 工程后,再导入

<dependency>
   <groupId>org.apache.tomcat.embed</groupId>
   <artifactId>tomcat-embed-jasper</artifactId>
   <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.servlet.jsp.jstl</groupId>
    <artifactId>jstl-api</artifactId>
    <version>1.2</version>
</dependency>

在src/main/resources/application.properties文件中配置JSP和传统Spring MVC中和view的关联

  • MVC

spring.view.prefix=/WEB-INF/views/
spring.view.suffix=.jsp

创建src/main/webapp/WEB-INF/views目录,JSP文件就放这里

注意

以上代码pom.xml中的javax.servlet.jsp.jstl是用于支持JSP标签库的,

在Web2.5的容器中没有问题,但当你的容器是Web3.0或以上版本时,就会出问题。这是个非常坑爹的问题。

javax.servlet.jsp.jstl会自动加载依赖servlet-api-2.5.jar,

而且会在实际运行时把支持Web3.0的3.1版本的javax.servlet-api覆盖掉。

即使你在pom.xml显示的在加入3.1版本的javax.servlet-api也没用。导致SpringBoot应用抛出Runtimeexception运行错误。

这是一个不可调和的矛盾,要么不用javax.servlet.jsp.jstl,要么不用Web3.0。

但绝大多数情况下,jstl标签库不是必须的,而Web3.0是必须的。

替代方式就是不用JSP,改用Themeleaf吧

思考: 在前端框架如此兴盛的当下,还有必要使用HTML模板吗?

Web相关配置

详见代码:WebMvcAutoConfiguration、WebMvcProperties

自动配置的ViewResolver

  • ContentNegotiatingViewResolver
    ​ 一个特殊的ViewResolver,自己不处理view,
    ​ 通过代理给不同的viewresolver处理不同的view
    ​ 拥有最高优先级
  • BeanNameViewResolver
    ​ 根据控制器的返回字符串,查找对应的view来渲染视图
  • InternalResourceViewResolver

自动配置的静态资源

类路径下的/static、/public、/resources、/META-INF/resources ,

这几个文件夹下的静态文件直接映射为/**,

一般放置js、css文件

可以通过http://localhost:8080/**来访问

webjar的 /META-INF/resources/webjars/ 下的的静态文件映射为 /webjar/**

Thymeleaf具体怎么使用,见Thymeleaf部分的笔记

自动配置的Formatter和Converter

Formatter和Converters是用于类型转化的

只要我们定义了Converter、GenericConverter、Formatter接口的实现类,

这些Bean就会自动注册到Spring MVC中

  • 自动配置HttpMessageConverters

编写好自定义的HttpMessageConverters后,注册为Bean,Spring Boot会自动注册

HttpMessageConverters是干嘛的?

答:请求的json字符串被@RequestBody转化为对象,就是HttpMessageConverters干的

@Bean
public HttpMessageConverters customerConverters() {
    HttpMessageConverter<?> h1 = ....
    HttpMessageConverter<?> h2 = ....
    return new HttpMessageConverters(h1,h2);
}

Formatter是对表单请求进行数据格式转换,

如果是JSON数据,可以直接在对象上使用@JsonFormatter、@JsonSerialize等注解

接管Spring Boot 的Web配置

如果Spring Boot的默认配置不符合要求,

则可以通过一个配置类加上@EnableWebMvc注解实现完全自己控制的MVC配置

如何既保留SpringBoot自动配置的便利,又增加自己额外的配置呢?

这个配置类需要继承WebMVCConfigurerAdapter,无需@EnableWebMvc

当然,配置类上@Configuration是肯定要添加的

注册Servlet、Filter、Listener

有两种方法:

  • 将Servlet、Filter、Listener注册为Spring Bean
  • 注册 ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean
@Bean
public ServletRegistrationBean servletRegistrationBean(){
    return new ServletRegistrationBean(new MyServlet(),"/xx/*");
}
@Bean
public FilterRegistrationBean filterRegistrationBean(){
    FilterRegistrationBean reg = new FilterRegistrationBean();
    reg.setFilter(new MyFilter());
    return reg;
}
@Bean
ServletListenerRegistrationBean<MyListener> servletListenerRegistrationBean(){
    return new ServletListenerRegistrationBean(MyListener)(new MyListener());
}


国际化

  • application.yml


spring:
  messages:
#  国际化文件
    basename: i18n/messages/messages

messages.properties
messages_en_US.properties
messages_zh_CN.properties

package com.zhidianfan.pig.yd.config;

import java.util.Locale;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

/**
 * 自定义国际化语言解析器
 */
public class MyLocaleResolver implements LocaleResolver {

    private static final String I18N_LANGUAGE = "i18n_language";
    private static final String I18N_LANGUAGE_SESSION = "i18n_language_session";

    @Override
    public Locale resolveLocale(HttpServletRequest req) {
        String i18n_language = req.getParameter(I18N_LANGUAGE);
        Locale locale = Locale.getDefault();
        if (!StringUtils.isEmpty(i18n_language)) {
            String[] language = i18n_language.split("_");
            locale = new Locale(language[0], language[1]);

            //将国际化语言保存到session  
            HttpSession session = req.getSession();
            session.setAttribute(I18N_LANGUAGE_SESSION, locale);
        } else {
            //如果没有带国际化参数,则判断session有没有保存,有保存,则使用保存的,也就是之前设置的,避免之后的请求不带国际化参数造成语言显示不对
//            在分布式环境中,使用Redis代替Session,Key:账户+language
            HttpSession session = req.getSession();
            Locale localeInSession = (Locale) session.getAttribute(I18N_LANGUAGE_SESSION);
            if (localeInSession != null) {
                locale = localeInSession;
            }
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest req, HttpServletResponse res, Locale locale) {

    }

}  








package com.zhidianfan.pig.yd.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 * web端配置信息
 *
 * @author danda
 */
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    /**
     * 配置自己的国际化语言解析器
     *
     * @return
     */
    @Bean
    public LocaleResolver localeResolver() {
        return new MyLocaleResolver();
    }

    /**
     * 配置自己的拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //super.addInterceptors(registry);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

请求中,带上 &i18n_language=zh_CN 用于设置当前语言环境

使用国际化

    @Autowired
    private MessageSource messageSource;

    String res = messageSource.getMessage("hello", null, locale);
    

将session存储改为Redis存储

import java.util.Locale;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.zhidianfan.pig.yd.utils.UserUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.LocaleResolver;

/**
 * 自定义国际化语言解析器
 */
public class MyLocaleResolver implements LocaleResolver {

    private static final String I18N_LANGUAGE = "i18n_language";
    private Logger log = LoggerFactory.getLogger(getClass());

    private RedisTemplate redisTemplate;
    private String local = "zh_CN";//默认环境 en_US 英文环境

    private UserUtils userUtils;

    public MyLocaleResolver(RedisTemplate redisTemplate, UserUtils userUtils) {
        this.redisTemplate = redisTemplate;
        this.userUtils = userUtils;
    }

    public MyLocaleResolver(RedisTemplate redisTemplate, String local) {
        this.redisTemplate = redisTemplate;
        this.local = local;
    }

    @Override
    public Locale resolveLocale(HttpServletRequest req) {
        String i18n_language = req.getParameter(I18N_LANGUAGE);
        Locale locale = null;
        String username = userUtils.getUserName();//用户名为唯一键
        String k = "LANGUAGE:" + username;
        if (!StringUtils.isEmpty(i18n_language)) {//客户请求中设置了值,以设置的国际化信息为准
            String[] language = i18n_language.split("_");
            locale = new Locale(language[0], language[1]);
            redisTemplate.opsForValue().set(k, locale);
            log.info("设置当前用户的语言环境:{}", i18n_language);

        } else {//客户未在请求中设置国际化参数

            Locale v = (Locale) redisTemplate.opsForValue().get(k);
            if (v != null) {
                locale = v;
                log.info("从缓存中获取语言环境:{}", v.getLanguage());
            } else {
                //设置默认值
                String[] language = local.split("_");
                locale = new Locale(language[0], language[1]);
                redisTemplate.opsForValue().set(k, locale);
                log.info("设置当前用户的默认语言环境:{}", local);

            }
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest req, HttpServletResponse res, Locale locale) {

    }

}      

SpringBoot使用Undertow

pom

首先要把tomcat依赖排除掉。

不过有时候,tomcat的依赖不一定是由web的starter引入进来的,这个需要自己看清楚仔细咯

<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>
<!--高性能web服务器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

然后引入undertowstarter即可

配置

# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
# 不要设置过大,如果过大,启动项目会报错:打开文件数过多

server.undertow.io-threads=16

# 阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程
# 它的值设置取决于系统线程执行任务的阻塞系数,默认值是IO线程数*8

server.undertow.worker-threads=256

# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分,不要设置太大,以免影响其他应用,合适即可

server.undertow.buffer-size=1024

# 每个区分配的buffer数量 , 所以pool的大小是buffer-size * buffers-per-region

server.undertow.buffers-per-region=1024

# 是否分配的直接内存(NIO直接分配的堆外内存)

server.undertow.direct-buffers=true

补充

Undertow认为它的运用场景是在IO密集型的系统应用中,并且认为多核机器是一个比较容易满足的点,Undertow初始化假想应用的阻塞系数在0.8~0.9之间,所以阻塞线程数直接乘了个8,当然,如果对应用较精确的估测阻塞系数,可以配置上去

@ControllerAdvice

旧版SpringMVC中的控制器参数校验:

    @ControllerAdvice
    public class ControllerCheckAdvice {
    
    
        private Logger log = LoggerFactory.getLogger(getClass());
    
        @ExceptionHandler(BindException.class)
        public ResponseEntity<Tip> checkRequest(BindException e) {
    
    
            log.info(e.getMessage());
    
            BindingResult bindingResult = e.getBindingResult();
    
            String errorMesssage = "校验失败:";
    
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                errorMesssage += fieldError.getField() + ":" + fieldError.getDefaultMessage() + ";";
            }
    
            Tip tip = new ErrorTip(400, errorMesssage.substring(0, errorMesssage.length() - 1));
            return ResponseEntity.badRequest().body(tip);
        }
    
    }


新版WebFlux异常校验

    @ControllerAdvice
    public class ControllerCheckAdvice {
    
    
        private Logger log = LoggerFactory.getLogger(getClass());
    
        @ExceptionHandler(WebExchangeBindException.class)
        public ResponseEntity<Tip> checkRequest(WebExchangeBindException e) {
    
    
            log.info(e.getMessage());
    
            BindingResult bindingResult = e.getBindingResult();
    
            String errorMesssage = "校验失败:";
    
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                errorMesssage += fieldError.getField() + ":" + fieldError.getDefaultMessage() + ";";
            }
    
            Tip tip = new ErrorTip(400, errorMesssage.substring(0, errorMesssage.length() - 1));
            return ResponseEntity.badRequest().body(tip);
        }
    
    }


Tomcat配置

在全局配置文件中
server.port=配置程序默认端口,默认8080
server.session-timeout=session过期时间,秒
server.context-path=配置访问路径,默认/

server.tomcat.uri-encoding=配置编码,默认UTF-8
server.tomcat.compression=是否开启压缩,默认off

所有的tomcat配置都在server.tomcat

  • 通过代码配置服务器信息
package org.zln.spb.config.prop;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.http.HttpStatus;
import java.util.concurrent.TimeUnit;
/**
 * Created by nbcoolkid on 2017-08-16.
 */
@Component
public class CustomerServletContainer implements EmbeddedServletContainerCustomizer {
    @Override
    public void customize(ConfigurableEmbeddedServletContainer configurable) {
        configurable.setPort(8080);
        configurable.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,"/404.html"));
        //配置session会话过期时间
        configurable.setSessionTimeout(10, TimeUnit.MINUTES);
    }
}

  • 直接Bean配置Tomcat容器
@Bean
public EmbeddedServletContainerFactory servletContainerFactory(){
    TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
    factory.setPort(8080);
    factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,"/404.html"));
    factory.setSessionTimeout(10, TimeUnit.MINUTES);
    return factory;
}

如果使用的是Jetty,则配置JettyEmbeddedServletContainerFactory;

如果使用的是Undertow,则配置UndertowEmbeddedServletContainerFactory

替换Tomcat

  • 使用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>

  • 使用Undertow
<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-undertow</artifactId>
</dependency>

文件上传

页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"></meta>
    <title>Title</title>
</head>
<body>
<form method="post" enctype="multipart/form-data" action="/upload">

    文件:<input type="file" name="file"/>

    <input type="submit" value="上传"/>

</form>
</body>
</html>

文件上传配置项

##默认支持文件上传.
spring.http.multipart.enabled=true
##支持文件写入磁盘.
spring.http.multipart.file-size-threshold=0
## 上传文件的临时目录
spring.http.multipart.location=/Users/zhangliuning/Gitee/java-ee/tmp
## 最大支持文件大小
spring.http.multipart.max-file-size=100Mb
## 最大支持请求大小
spring.http.multipart.max-request-size=1000Mb

Part方式上传

...
import javax.servlet.http.Part;
...

@Controller
@Slf4j
public class WebController {

    /**
     * 文件上传
     *
     * @param file
     * @return
     */
    @RequestMapping("/upload")
    @ResponseBody
    public String fileUpload(@RequestPart Part file) {
        // 获取文件名
        String fileName = file.getSubmittedFileName();
        log.info("上传的文件名为:" + fileName);
        // 获取文件的后缀名
        String suffixName = fileName.substring(fileName.lastIndexOf("."));
        log.info("上传的后缀名为:" + suffixName);
        // 文件上传路径
        String filePath = "/Users/zhangliuning/Gitee/java-ee/功能点/test001-sp-upload/";
        // 解决中文问题,liunx下中文路径,图片显示问题
        fileName = fileName + "-" + UUID.randomUUID() + suffixName;
        File dest = new File(filePath, fileName);
        // 检测是否存在目录
        if (!dest.getParentFile().exists()) {
            dest.getParentFile().mkdirs();
        }
        try {
            file.write(dest.getAbsolutePath());
            log.info(dest.getAbsolutePath());
            return "上传成功";
        } catch (IllegalStateException | IOException e) {
            e.printStackTrace();
        }
        return "上传失败";
    }

}

这里使用Part类型参数接收请求,如果使用的是MultipartFile类型,
getSubmittedFileName相当于是getOriginalFilename,
write相当于是transferTo方法,Part类型是Servlet3.0提供的

MultipartFile方式上传

/**
 * 文件上传
 * @param part
 * @return
 * @throws IOException
 */
@RequestMapping("/upload.do")
public String upLoad(@RequestPart("file") MultipartFile part) throws IOException {
    File file = new File("C:\\Users\\nbcoolkid\\Documents\\git\\programlearing\\java-web\\web-manager-01\\src\\upLoadDir\\");
    if (!file.exists()){
        file.mkdir();
    }
    File file1 = new File(file,UUID.randomUUID().toString()+part.getOriginalFilename());
    part.transferTo(file1);
    logger.info("文件上传至:"+file1.getAbsolutePath());
    return "main";
}

SSL与HTTPS

keytool -genkey -alias tomcat  -storetype PKCS12 -keyalg RSA -keysize 2048  -keystore keystore.p12 -validity 3650
1.-storetype 指定密钥仓库类型 
2.-keyalg 生证书的算法名称,RSA是一种非对称加密算法 
3.-keysize 证书大小 
4.-keystore 生成的证书文件的存储路径 
5.-validity 证书的有效期

在application.properties中添加如下代码:

server.ssl.key-store=keystore.p12
server.ssl.key-store-password=111111
server.ssl.keyStoreType=PKCS12
server.ssl.keyAlias:tomcat

第一行指定签名文件,第二行指定签名密码,第三行指定密钥仓库类型,第四个是别名。OK,这样配置完成之后我们就可以通过HTTPS来访问我们的Web了

HTTP自动转向HTTPS

@Bean
    public EmbeddedServletContainerFactory servletContainer() {
        TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint constraint = new SecurityConstraint();
                constraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection = new SecurityCollection();
                collection.addPattern("/*");
                constraint.addCollection(collection);
                context.addConstraint(constraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(httpConnector());
        return tomcat;
    }

    @Bean
    public Connector httpConnector() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        //Connector监听的http的端口号
        connector.setPort(8080);
        connector.setSecure(false);
        //监听到http的端口号后转向到的https的端口号
        connector.setRedirectPort(8443);
        return connector;
    }

这个时候当我们访问http://localhost:8080的时候系统会自动重定向到https://localhost:8443这个地址上。这里的Connector实际就是我们刚刚接触jsp时在xml中配置的Tomcat的Connector节点

Favicon配置

将自己的favicon.ico放置在类路径根目录、类路径META-INF/resources/、类路径resources/、类路径static/或类路径public/

关闭:spring.mvc.favicon.enable=false

WebSocket

添加WebSocket的starter

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

一般不会使用WebSocket,而是使用它的子协议STOMP

SpringBoot对内嵌的Tomcat(7、8)、Jetty9、Undertow使用WebSocket提供了支持

广播模式

  • WebSocketConfig
package org.zln.spb.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
/**
 * WebSocket相关配置
 * Created by nbcoolkid on 2017-06-26.
 */
@Configuration
//使用STOMP协议来传输基于代理的消息
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
/**
     * 注册STOMP协议的节点,并映射指定的URL
     * @param stompEndpointRegistry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
//        注册一个STOMP的endpoint,并且指定使用SocketJS协议
        stompEndpointRegistry.addEndpoint("endpointWisely").withSockJS();
    }
/**
     * 配置消息代理
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
//        广播式应配置一个topic消息代理
        registry.enableSimpleBroker("/topic");
    }
}

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>广播式WebSocket</title>
    <script th:src="@{js/sockjs.min.js}"></script>
    <script th:src="@{js/stomp.js}"></script>
    <script th:src="@{js/jquery-3.1.1.js}"></script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #e80b0a;">Sorry,浏览器不支持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 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';
//        $("#connect").disabled = connected;
//        $("#disconnect").disabled = !connected;
        $("#response").html();
    }
    function connect() {
        var socket = new SockJS('/endpointWisely');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            setConnected(true);
            console.log('Connected:' + frame);
            stompClient.subscribe('/topic/getResponse', function (response) {
                showResponse(JSON.parse(response.body).responseMessage);
            })
        });
    }
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log('Disconnected');
    }
    function sendName() {
        var name = $('#name').val();
        console.log('name:' + name);
        stompClient.send("/welcome", {}, JSON.stringify({'name': name}));
    }
    function showResponse(message) {
        $("#response").html(message);
    }
</script>
</body>
</html>

package org.zln.learning.springboot.cfg;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
 * Created by sang on 16-12-22.
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/ws").setViewName("/ws");
    }
}

package org.zln.learning.springboot.controller;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.zln.learning.springboot.domain.WiselyMessage;
import org.zln.learning.springboot.domain.WiselyResponse;
/**
 * Created by nbcoolkid on 2017-08-17.
 */
@Controller
public class WsController {
@MessageMapping("/welcome")
    //SendTo 发送至 Broker 下的指定订阅路径
    @SendTo("/topic/getResponse")
    public WiselyResponse say(WiselyMessage message) throws InterruptedException {
        Thread.sleep(3000);
        return new WiselyResponse("Welcome," + message.getName() + " !");
    }
}

点对点模式

  • WebSocketConfig
package org.zln.learning.springboot.cfg;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
/**
 * Created by nbcoolkid on 2017-08-17.
 */
@Configuration
//开启使用STOMP协议来传输基于代理的消息
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {//注册STOMP协议的节点,并映射指定URL
//        注册一个STOMP的endpoint,并指定使用SocketJS协议
        stompEndpointRegistry.addEndpoint("/endpointChat").withSockJS();
    }
@Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {//配置消息代理
//        点对点配置一个消息队列
        registry.enableSimpleBroker("/queue");
    }
}

  • WebSecurityConfig
package org.zln.learning.springboot.cfg;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
 * Created by sang on 16-12-22.
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //设置拦截规则
                .antMatchers("/")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                //开启默认登录页面
                .formLogin()
                //默认登录页面
                .loginPage("/login")
                //默认登录成功跳转页面
                .defaultSuccessUrl("/chat")
                .permitAll()
                .and()
                //设置注销
                .logout()
                .permitAll();
    }
@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("lenve").password("111").roles("USER")
                .and()
                .withUser("sang").password("222").roles("USER");
    }
@Override
    public void configure(WebSecurity web) throws Exception {
        //设置不拦截规则
        web.ignoring().antMatchers("/resources/static/**");
    }
}

  • WebMvcConfig
package org.zln.learning.springboot.cfg;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
 * Created by sang on 16-12-22.
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("/login");
        registry.addViewController("/chat").setViewName("/chat");
    }
}

  • WsController
package org.zln.learning.springboot.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.zln.learning.springboot.domain.WiselyMessage;
import org.zln.learning.springboot.domain.WiselyResponse;
import java.security.Principal;
/**
 * Created by nbcoolkid on 2017-08-17.
 */
@Controller
public class WsController {
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    @MessageMapping("/chat")
    public void handleChat(Principal principal, String msg) {
        if (principal.getName().equals("sang")) {
            messagingTemplate.convertAndSendToUser("lenve", "/queue/notifications", principal.getName() + "给您发来了消息:" + msg);
        } else {
            messagingTemplate.convertAndSendToUser("sang", "/queue/notifications", principal.getName() + "给您发来了消息:" + msg);
        }
    }
}

  • login.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="UTF-8" />
    <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>

  • chat.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>聊天室</title>
    <script th:src="@{js/sockjs.min.js}"></script>
    <script th:src="@{js/stomp.js}"></script>
    <script th:src="@{js/jquery-3.1.1.js}"></script>
</head>
<body>
<p>聊天室</p>
<form id="sangForm">
    <textarea rows="4" cols="60" name="text"></textarea>
    <input type="submit" value="发送"/>
</form>
<script th:inline="javascript">
    $("#sangForm").submit(function (e) {
        e.preventDefault();
        var textArea = $("#sangForm").find('textarea[name="text"]');
        var text = textArea.val();
        sendSpittle(text);
        textArea.val('');
    });
    var sock = new SockJS("/endpointChat");
    var stomp = Stomp.over(sock);
    stomp.connect('guest','guest',function (frame) {
        stomp.subscribe("/user/queue/notifications", handleNotification);
    });
    function handleNotification(message) {
        $("#output").append("<b>Received: "+message.body+"</b><br/>")
    }
    function sendSpittle(text) {
        stomp.send("/chat", {}, text);
    }
    $("#stop").click(function () {
        sock.close();
    });
</script>
<div id="output"></div>
</body>
</html>

基于BootStrap、AngularJS的现代Web应用

现代B/S系统特点

  • 单页面应用
  • 响应式布局
  • 数据导向
    ​ 页面的数据是通过消费后台的REST服务来实现的,而不是通过服务器渲染的动态页面(如:JSP)
    ​ 一般数据交互格式是JSON

war还是jar?

传统上,web项目最终是一个war,然后部署到web容器中

SpringBoot可以将web项目打成jar。即是包含页面、资源文件的web项目也行

建议打成jar,这种方式更利于在Docker中运行

猜你喜欢

转载自blog.csdn.net/m0_37208669/article/details/85232869