SpringBoot

1 SpringBoot

1.1 关于

  • 约定大于配置:简化M-V-C开发模式;使编码、配置、部署、监控变得简单

    • 为基于Spring的开发提供更快的体验,创建可以独立运行的Spring应用

    • 直接内嵌Servlet容器:Tomcat或Jetty服务器,可以打包成war文件,也可以以jar包形式独立运行:java -jar xxx.jar

    • 提供pom.xml文件来简化Maven配置:starters中自动依赖与版本控制;尽可能的根据项目依赖来自动配置Spring框架

    • 提供可以直接在生产环境中使用的功能,如性能指标、应用信息和应用健康检查;提供http,ssh,telnet等对运行时项目进行监控

    • 开箱即用,无需xml文件配置,不借助代码生成来实现;还可以修改默认值来满足特定需求

    • 能与主流框架集成;其他大量项目如SpringCloud都基于SpringBoot

  • 缺点:

    • 依赖太多;缺少服务的注册和发现等解决方案;缺少监控集成方案,安全管理方案

1.2 主程序/启动类

  • spring-boot-starter:场景启动器,用于帮助导入各个场景所需依赖的组件(jar),如spring-boot-starter-web,aop,cache,data-jdbc,data-jpa,data-redis,data-rest,freemarker,jdbc,json,test等;这样免去了在xml中定义bean对象

  • @SpringBootApplication:标注启动类,该程序是一个SpringBoot应用

    • @SpringBootConfiguration:标注配置类,同Spring的@Configuration

    • @EnableAutoConfiguration:开启自动配置,其原理(核心):(为什么配置文件能定义那么多属性,为什么它们能生效)SpringBoot启动时,加载主配置类,开启自动配置功能;利用@Import导入各种组件,包括配置类

      • @Import({AutoConfigurationImportSelector.class}):导入组件,一个包含了众多自动配置类xxxAutoConfiguration的List;如AopAutoConfiguration,开发无需再手动编写配置和注入这些组件的工作;组件从jar包下MATA-INF/spring.factories中读取@EnableAutoConfiguration标注的组件,如视图解析器等

        • @ConditionalOnXXX:扩展的Spring底层注解@Conditional,用于判断条件是否满足,满足则该类可用;如在配置类上的@ConditionalOnWebApplication,当为web应用时本配置类有效;(配置debug=true可以在日志中看生效了的配置类)

        • 配置类的作用:创建bean,即为容器添加各种组件(@Bean),这些组件的属性来自对应的properties类,这些属性就对应配置文件中的每一个key(同自定义配置后,映射到自定义类中属性一样)

      • @AutoConfigurationPackage:自动配置包;其下@Import({Registrar.class});两个注解就把启动类所在包及子包中的组件扫描到容器,所以特别注意其位置

  • main():运行SpringApplication.run();启动流程:

    • run()中调用构造器,初始化SpringApplication实例:

      public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
          this.resourceLoader = resourceLoader;
          Assert.notNull(primarySources, "PrimarySources must not be null");
          // main方法中的args参数即主类;可接收命令行启动时添加的参数
          this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
          // 确认当前应用程序的类型;一般都是Servlet
          this.webApplicationType = WebApplicationType.deduceFromClasspath();
          // 加载ApplicationContextInitializer初始化类获取Spring工厂实例
          setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
          // 加载ApplicationListener类,监听类
          setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
          // 获取main方法所在类
          this.mainApplicationClass = deduceMainApplicationClass();
      }
      
      private Class<?> deduceMainApplicationClass() {
          try {
              StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
              for (StackTraceElement stackTraceElement : stackTrace) {
                  if ("main".equals(stackTraceElement.getMethodName())) {
                      return Class.forName(stackTraceElement.getClassName());
                  }
              }
          }
          catch (ClassNotFoundException ex) {
              // Swallow and continue
          }
          return null;
      }
    • 执行run():

      • 从同路径下获取SpringApplicationRunlisteners,启动它们的starting()

      • 配置环境ConfigurableEnvironment;回调listener的environmentPrepared()

      • 创建打印Banner,即控制台输出的那个图案

      • 创建ApplicationContext,即IoC容器;初始化容器

  • javax.servlet.ServletContainerInitializer:封装web.xml

    • 这个类会在web容器启动阶段被回调,可以在onStartup方法里做一些servlet、filter、listener的注册等操作。springboot在META-INF/services下配置了这个类,让整个web容器启动后可以找到并启动这个类,即它把web.xml封装成了配置类

1.3 配置文件与配置类

  • 全局配置文件:.yml或.properties;修改SpringBoot自动配置的默认值

  • 语法:

    • 以数据为中心,比xml(标签)和json更适合做配置文件

    • 默认无引号,如果加了双引号,特殊字符依旧有效,如/n换行,而单引号则直接原样输出

    • 集合/数组:元素前使用-列举;或者数组用逗号分隔开的枚举,集合再用[]包含数组

    • 占位符:

      • 配合使用预定义方法,如${random.int}或${random.uuid}随机数

      • 引用已经存在的key,如${key};若key不存在,则key为默认值;使用:可自定义默认值;如${key: value}

    • properties配置文件可能乱码,与IDEA默认UTF8编码不符,需勾选file encoding下的transparent native-to-acscii convension即转码本地到ASICC

  • 使用配置:

    • @Value():Spring单一注入,用于属性或setter()上;需要一个一个的注入;支持SpringEL;适用于业务中获取某个值

    • @ConfigurationProperties(prefix=“配置文件中属性”):SringBoot把配置和Bean相关联来实现;修改SpringBoot自动配置的默认值有效就是此理,所以定义值后能自动注入;支持:

      • 批量注入;适用于映射整个Bean

      • 松散绑定,如userName与user-name一致

      • JSR303数据校验,如@Validated开启校验支持后,属性上可使用@Null,@Min,@Email等控制输入规范

      • 对象、集合等复杂类型如对象封装

  • 添加组件:

    • @ImportResource(loactions={"classpth:bean.xml}):加载Spring的配置文件

    • @Configuration:推荐使用配置类;配合@Bean

  • 多环境配置

    • 文件名前缀固定的前提下,不同标识指定不同环境,如application-test.yml与application-dev.yml;默认的application.yml中spring.profiles.active=dev激活指定文件

    • yml支持一个文件中的配置块:---分开的配置,如配置两个server.port;每个块中单独指定激活的文件

    • 命令行指定激活文件:edit configuratuin设置中设置program arguments值为--spring.profiles.active=dev;或者启动最终打包项目时的启动命令后加此命令

  • 多数据源配置

    • dataSource下配置多个数据源的属性信息

    • 分别编写两个配置类对应两个数据源,mapper包扫码各自的,service层调用各自的dao层

    # thymeleaf配置:即ThymeleafAutoConfiguration配置类
    #server:
    #  servlet:
    #    context-path:
    ​
    test:
    id: 1
    name: user
    max: true
    # 占位符:${SpEL},当SpEL不存在,则将他解析为等值的字符串,后接冒号可自定义默认值
    #  name: zg${test.id}h
    ​
    #===========================================
    # 多profile配置文件
    # 默认为此文件;当需要切换环境如测试环境,开发环境,生产环境时,可定义多个profile(注意命名)
    # 然后在此指定使用目标配置文件激活:
    #  (另一种激活方法:命令行参数:在edit Configurations-spring boot里面的Program arguments中输入命令:--spring.profiles.active=dev)
    #   (另另一种激活方法:虚拟机参数:在edit Configurations-spring boot里面的VM options中输入:-Dspring.profiles.active=dev)
    #   (另另另一种激活方法:命令行参数:在项目打包后,在命令窗口启动项目时,在最后添加同上的命令行参数命令)
    #spring:
    #  profiles:
    #    active: dev
    ​
    # yml文件还有文档块写法:---隔开
    #server:
    #  port: 8080
    #spring:
    #  profiles:
    #    active: dev
    #---
    #server:
    #  port: 8081
    #spring:
    #  profiles: dev
    #---
    #server:
    #  port: 8082
    #spring:
    #  profiles: prod
    ​
    #================================================================
    # 配置文件加载位置:
    # springboot的4个默认有效位置:优先级依次降低;内容互补,高优先级配置会覆盖低优先级中存在的,而不是有高优先级就不加载低优先级的
    #-file:/config/,即放在与src同级目录config中的文件
    #-file:/,即放在与src同级的文件
    #-classpath:/config,即放在resources下config下的文件
    #-classpath:/,即放在resources下的文件,项目最初始时的默认模样
    # 指定任意文件为配置文件:配置spring.config.location=绝对路径;运维常用此方式,在打包项目后启动命令加此命令;(命令行参数不仅这些,还可以是任意配置,如加--server.prot=8081)
    #项目内部配置
    # 加载指定的配置文件:@PropertySource(value="classthpath:xxx");application.yaml/properties是默认能识别的配置文件,自定义的不能识别,需要手动指定加载
    #spring:
    #  config:
    #    location:
    #项目外部配置:参考官方文档
    ​
    logging:
     file:
     name: log/springboot.log
    ​
    spring:
     messages:
     basename: i18n/message
     #禁用模板引擎的缓存,默认为true;开发时关闭,应用上线时开启以降低数据库压力
     thymeleaf:
      cache: false
     datasource:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/dbtest?useUnicode=true&characterEncoding=utf-8
      username: root
      password: 123456
      #使用自定义数据源druid
      type: com.alibaba.druid.pool.DruidDataSource
      #    schema:
      #      - classpath: sql/user.sql
      #    schema用来从IDE中生成数据库中的表:user.sql是位于templates/sql下的一张表
      #黄色背景的说明是自定义属性,不能与Bean绑定;代码提示出来的都是默认的hikari,dbcp2,tomcat连接池;
      #使用自定义的Druid,需要向容器添加映射组件@Configuration
      #如与DataSource相关的DataSourceProperties中存在上面无背景的属性,不存在下面黄色背景的属性;需要自定义映射
      initialSize: 5
      minIdle: 5
      maxActive: 20
      maxWait: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      filters: stat,wall
      useGlobalDataSourceStat: true
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
      #    文件上传:单个文件最大大小;上传的文件总大小
     servlet:
      multipart:
        max-file-size: 200MB
        max-request-size: 500MB
      #初始大小;最小;最大;连接等待超时时间(毫秒);检测(需要关闭的空闲连接)一次的间隔,;个连接在池中的最小生存时间
      #检测数据库连通性(不同数据库的validationQuery值不同,返回一条不为空的SQL);获取连接时测试连接是否有效(都为true时,testOnBorrow优先级高)
      #是否打开PSCache(PreparedStatements 缓存);打开时设置缓存大小;监控统计拦截的filters(监控统计stat,防御SQL注入wall,日志log4j(2.0版本后不要这个了?))
     mybatis:
      config-location: classpath:mybatis/config/mybatis-config.xml
      mapper-locations: classpath:mybatis/mapper/*.xml

1.4 数据访问

  • SpringBoot默认的DataSource:org.apache.tomcat.jdbc.pool.DataSource、HikariDataSource、BasicDataSource

  • 自定义DataSource:配置返回DataSource的Bean,return dataSourceProperties.initializeDataSourceBuilder().build()

    • Druid:引入依赖;配置spring.dateSource.type为druid(屏蔽默认的Tomcat.jdbc);配置该类Bean;配置它的数据监控/拦截器

    • Mybatis:MybatisAutoConfiguratuin;@Mapper定义或@MapperScan扫描

    • Spring Data JPA:

      • @Entity标注映射到数据库表的Bean;@Table映射表名;@Id标注Id;@GeneratedValue标注主键增长策略

      • 编写Dao接口:继承JpaRepository;已经有了基本的CURD方法

      • 复杂的CURD及SQL语句编写

      • https://www.cnblogs.com/TechSnail/p/7242777.html

1.5 国际化

  • 文件:i18n目录下配置默认的、en_US、zh_CN三个文件;页面切换语言环境后会自动适配对应文件中的值

  • ResourceBundleMessageSource:文件管理;MessageSourceAutoConfiguration:配置类

  • 获取:前端使用#{key}

  • Locale:区域信息对象;默认区域信息解析器根据请求头中环境信息来设置;可自定义

1.6 WebMvcAutoConfiguration

  • 静态资源:

    • 可直接页面访问;如导入依赖bootstrap

    • 静态资源的访问路径:classpath/下的/resources或/static或/public下(优先级依次降低),或/webjars/下即maven依赖的META-INF/resources/webjars下的jar包可访问,因此pom也可导入前端jar包且页面访问时,打开了源码;可配置文件中自配置资源访问路径

    • 引用:如引用css文件,< link th:href="@{/webjars/bootstrap/4.0.0/css/bootstrap.css}">

    • 首页/欢迎页:源码getIndexHtml()设置为"访问路径+index.html"

  • 动态资源/模板引擎

    • 位于/templates下的文件,需要编译(模板渲染)而不能直接打开;只能通过Controller访问且默认为index.html

1.7 拦截器

  • 定义:实现HandlerInterceptor的preHandle(),postHandler()等方法,如登录拦截器,在登录之前的判断等

  • 注册:实现HandlerInterceptorAdapter的addInterceptor(拦截器),addPathPatterns("拦截路径"),excludePathPatterns(”忽略的路径“)等

1.8 控制器

  • springboot自动配置了:
    • ViewResolver视图解析器,它根据方法返回值映射到视图对象View,View决定如何渲染(重定向,转发)
    • Converter转换器:页面数据提交都是文本(如true),需要类型转为Integer,boolean等
    • Formatter格式化器:格式化时间等(包括类型转换和格式化)
    • 可以自定义各种器,如请求处理器,类型转换器(如前端form表单数据转换到MVC中的对象),视图解析器,格式化器,网页图标,消息转换器(如转换http请求和响应),外部数据初始化以及扩展MVC等:实现ViewResolver,然后配置组件@Bean
  • SpringMVC扩展:@Configuration配置类代替配置文件;即自定义MVC功能,同Spring中定义配置类一样;特殊的,@Configuration标注类后,实现接口WebMvcConfigurer,以此自定义SpringMVC的配置类功能,如自定义视图解析器;不能添加@EnableWebMvc,它会让默认配置失效,即全面接管了SpringMVC
  • restful风格CURD:以请求方式区别,且不体现请求动作如add:user(get),user(post),user/{id}(put),user/{id}(delete)
    • 普通CURD:getUser、addUser?xxx、updateUser?id=xxx、deleteUser?id=xxx
  • SpringBoot返回资源页面规则:prefix:templates/,suffex:/.html,方法返回值拼接在中间
  • 针对post请求乱码问题:SpringMVC提供了过滤器(SpringBoot不同):CharacterEncodingFilter,配置filter
@Controller
public class SpringMVCController {
    @Autowired
    private JdbcTemplate jdbcTemplate;
​
    @ResponseBody
    @GetMapping("/hello")
    public String hello() {
        return "hello world!";
    }
​
    // 如果不写方法,则默认访问的是static下的index.html
    // @GetMapping({"/", "/login.html"})
    // public String getIndex() {
    //     return  "login";
    // }
​
    @GetMapping("/user")
    public String toLogin() {
       return "login";
    }
​
    @PostMapping("/login")
    public String login(@RequestParam("userName") String userName,
                        @RequestParam("password") String password,
                        Map<String, String> message) {
        if (!StringUtils.isEmpty(userName) && "123456".equals(password)) {
            return "user/main";
        }
        message.put("message", "用户名或密码错误,请检查!");
        return "ymldefinetest";
    }
 ​​
    /**
     * 使用原生JDBC连接和操作数据库:查询所有后直接以文本显示在页面上
     */
    @ResponseBody
    @GetMapping("/users")
    public List<Map<String, Object>> queryUser() {
        String sql = "select * from user";
        List<Map<String, Object>> users = jdbcTemplate.queryForList(sql);
        return users;
    }
​
    // 文件上传:由于引入了spring-security,post请求会被拦截,页面403;暂时未处理
    // 批量上传,将参数改为数组,命名和上传功能放到对该数组的循环中
    @ResponseBody
    @PostMapping("/upload")
    public String uploadFile(HttpServletRequest request, MultipartFile multipartFile) { // 注意参数名和前端中name值一致
        // 指定文件上传后在服务器的存储路径
        // 创建upload文件夹
        String direction = request.getServletContext().getRealPath("/upload");
        File file = new File(direction);
        if (!file.exists()) {
            file.mkdirs();
        }
        // 指定文件保存在服务器时的名称,判重
        String fileSuffix = multipartFile.getOriginalFilename().substring(multipartFile.getOriginalFilename().lastIndexOf("."));
        String fileName = UUID.randomUUID().toString() + fileSuffix;
        // 创建文件
        File files = new File(file + "/" + fileName);
        // 上传
        try {
            multipartFile.transferTo(files);
        } catch (IOException e) {
            e.printStackTrace();
            return "上传失败!";
        }
        return "上传成功!";
    }
​
    // 会话管理之自设置Cookie:服务器发送到浏览器并保存在浏览器端的一小段数据;浏览器下次访问此服务器时自动携带该cookie;仅存字符串类型数据
    @GetMapping("/cookie")
    public String setCookie(HttpServletResponse response) {
        // 创建cookie,设置cookie的有效范围(访问哪些路径时有效),生存时间;发送cookie等
        Cookie cookie = new Cookie("cookie", "cookie");
        cookie.setPath("/somePath");
        cookie.setMaxAge(600);
        response.addCookie(cookie);
        return "/cookies";
    }
    // 会话管理之自设置Session:服务器端记录客户端信息;安全但增加了内存消耗;
    // 浏览器cookie中保存sessionId(页面上显示为jsessionid),发送到服务器匹配session;可存任意类型数据
    @GetMapping("/session")
    @ResponseBody
    public String getSession(HttpSession session) {
        String id = (String) session.getAttribute("id");
        String name = (String) session.getAttribute("name");
        return "/sessions";
    }
}

1.9 错误处理

  • ErrorMvcAutoConfiguration:错误处理的自动配置类;添加的组件中设置了响应规则,如产生错误后去到页面template/error下,DefaultErrorViewResolver映射到指定错误页面,设置错误响应页面的样式;信息包含了时间戳、状态码、错误提示、异常对象及信息等;错误页面不在templates下,则查找statics下,都没有则得到SpringBoot默认错误提示页面(一段写在后端的页面代码)

  • 错误处理:

    • 使用默认规则:即错误页面放于/error下,命名以4或5开头;优先匹配精确的状态码如404.html

    • 自定义异常处理器:@ControllerAdvice标注的类,@ExceptionHandler标注错误处理逻辑,包括写入异常信息(异常信息也可自定义实现Exception),返回的视图等;实现DefaultErrorAttributes的getErrorAttributes()自定义错误属性,可可以包含异常信息

1.10 日志记录

  • 实现类logback + 调用顶层抽象slf4j

  • 导入spring-boot-starer-logging

  • 日志级别:.trace()、.debug()、.info()默认、.warn()、.error();仅打印该级别及以后的信息

1.11 单元测试

  • @RunWith(SpringRunner.class)与@SpringBootTest:可导入Junit做单元测试

1.12 部署

  • pom.xml中添加插件< build>< plugin>spring-boot-maven-plugin,即将应用打包为可执行jar

  • 执行maven-package,最终打包在target下;执行java -jar xxx.jar启动

1.13 缓存

  • @EnableCaching开启缓存(bean需要实现可序列化)

    • Spring注解;使用注解时,会触发一个post processor,这会扫描每一个bean,查看是否已经存在注解对应的缓存。如果找到,则会自动创建一个代理拦截方法调用,使用缓存的bean执行处理

  • 使用SpringBoot Data Redis:lettuce替换了jedis,集成Redis

    • 配置各种属性(实质就是RedisTemplate的属性)即可

       spring:
           redis:
               host: 127.0.0.1
               port: 6379

    • 开发中自定义RedisTemplate,如自定义序列化机制;然后编写工具类RedisUtil,注册该类即可,而不是使用原生API

  • @Cacheable(value=“sampleCache”), key="")

    • 用于方法上;调用方法时会从value为sampleCache的缓存(缓存本质是一个map)中查询key为id的值,如果不存在,则执行实际的方法(即查询数据库等服务逻辑),并将执行的结果存入缓存中,否则返回缓存中的对象。key默认是只有一个参数时方法的参数,多参数时需指定

1.14 定时器/任务调度

  • core:设置定时执行的表达式,如 0 0/5 * * * ?每隔5分钟执行一次

  • zone:执行时间所在时区

  • fixedDelay / fixedDelayString:固定延迟时间执行,上个任务完成后,延迟多久执行

  • fixedRate / fixedRateString:固定频率执行,上个任务完成后,多久后开始执行\

  • initialDelay / initialDelayString:初始延迟时间

  • 定时任务表达式:秒、分、时、天、月...,取值为各自默认范围,如秒为0-59

    • 2 * * * * ?:每一分钟的第2秒执行;其他同理,如2 35 18 * * ?为每天的18时35分2秒时执行

    • 0/2 * * * * ?:等步长序列,从0秒开始,每隔2秒执行,即步长为2;如1/15为从1秒开始,1,16,31执行;其他同理

    • 1-2 * * * * ?:范围,每分钟的1-2秒内执行

    • 1,2 * * * * ?:枚举,每分钟的1秒和2秒执行

    • *匹配任意,?匹配占位

  • Spring3.0后自带的Scheduled定时任务器

    • @EnableScheduling:开启对定时任务的支持

    • @Scheduled:在方法上声明需要执行的定时任务

      @Component
      @Configuration
      @EnableScheduling
      public class ScheduleUtil {
           /**
            * 测试定时任务:每隔2秒执行
            * 由控制台输出,可知每隔两秒执行一次方法
            */
           @Scheduled(cron = "0/2 * * * * ?")
           public void testScheduled() {
             System.out.println("自带触发定时器任务:" + new Date());
           }
      }

  • 第三方定时任务调度框架Quartz(复杂)

    import org.quartz.*;
    import org.quartz.impl.StdSchedulerFactory;
    /**
     * 定时任务类:第三方任务调度框架Quartz
     */
    public class ScheduleQuartzUtil implements Job {
        /**
         * 唯一重写的方法:任务触发时执行
         * @param jobExecutionContext
         * @throws JobExecutionException
         */
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            System.out.println("第三方触发定时器任务:" + new Date());
        }
    ​
        // Quartz的简单使用;与SpringBoot整合参见配置类
        public static void main(String[] args) throws SchedulerException {
            // 1. 创建Job对象,即定义定时任务的内容
            JobDetail jobDetail = JobBuilder.newJob(ScheduleQuartzUtil.class).build();
    ​
            // 2. 创建Trigger对象,即定义触发时间,等同于@Schedule()的cron
            // Quartz提供了简单的时间定义封装SimpleScheduleBuilder
            Trigger trigger = TriggerBuilder.newTrigger().withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(2)).build();
            // 复杂的自定义定时时间CronScheduleBuilder
            // Trigger trigger = TriggerBuilder.newTrigger().withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?")).build();
     ​
            // 3. 创建Scheduler对象,联结定时任务和时间
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            scheduler.scheduleJob(jobDetail, trigger);
            // 4. 启动定时器
            scheduler.start();
        }
    }
    /**
     * 第三方定时调度任务框架Quartz的复杂应用:整合SpringBoot
     */
    @Configuration
    @EnableScheduling
    public class QuartzConfig {
        // 1. 创建Job对象,即定义定时任务的内容
        @Bean
        public JobDetailFactoryBean getJobDetailFactoryBean() {
            JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
            factoryBean.setJobClass(ScheduleQuartzUtil.class);
            return factoryBean;
            // 注意此处,ScheduleQuartzUtil的创建交给了Factory,其源码采用的是反射机制构建,即没有被Spring容器管理,容器中不存在该实例
            // 因此当此config注入其他Bean即有@Autowired属性时,报空指针异常;
            // 解决方案:重写创建定时器实例的类AdaptableJobFactory的方法,在返回该实例之前,手动将其注入到Spring容器
            // @Component("myAdaptableJobFactory")
            // class MyAdaptableJobFactory extends AdaptableJobFactory {
            //     @Autowired
            //     private AutowireCapableBeanFactory autowireCapableBeanFactory; // 此对象帮助将自定义Bean注入到容器
            //     @Override
            //     protected Object createJonInstance(TriggerFiredBundle bundle) throws Exception {
            //         // return super.createJobInstance(bundle); // 修改此处为下面的
            //         Object object = super.createJobInstance(bundle);
            //         this.autowireCapableBeanFactory.autowireBean(object);
            //         return object;
            //     }
            // }
            // 然后在第三步,即创建Scheduler对象时,传入此实例,即factoryBean.setTriggers(myAdaptableJobFactory);
        }
    ​
        // 2. 创建Trigger对象,即定义触发时间,等同于@Schedule()的cron
        // Quartz提供了简单的时间定义封装SimpleScheduleBuilder
        @Bean
        public SimpleTriggerFactoryBean getSimpleTriggerFactoryBean(JobDetailFactoryBean jobDetailFactoryBean) {
            SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
            factoryBean.setJobDetail(jobDetailFactoryBean.getObject()); // 关联定时任务
            factoryBean.setRepeatInterval(2000); // 设置执行的毫秒数
            factoryBean.setRepeatCount(5); // 设置执行的次数
            return factoryBean;
        }
        // 复杂的自定义定时时间CronScheduleBuilder
        @Bean
        public CronTriggerFactoryBean getCronTriggerFactoryBean(JobDetailFactoryBean jobDetailFactoryBean) {
            CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean();
            factoryBean.setJobDetail(jobDetailFactoryBean.getObject());
            factoryBean.setCronExpression("0/2 * * * * ?");
            return factoryBean;
        }
    ​
        // 3. 创建Scheduler对象,联结定时任务和时间
        @Bean
        public SchedulerFactoryBean getSchedulerFactoryBean(SimpleTriggerFactoryBean simpleTriggerFactoryBean) {
            SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
            factoryBean.setTriggers(simpleTriggerFactoryBean.getObject());
            // factoryBean.setTriggers(cronTriggerFactoryBean.getObject());
            return factoryBean;
        }
    ​
        // 4. 启动定时器:开启@EnableScheduling
    }

1.14 前后端数据交互

  • 常见数据接收和发送,参见SpringMVC

  • 系统中发送http请求并传递json数据(常见场景为调用第三方请求并写入请求参数)

    • RestTemplate / HttpClient / OkHttpClient:同步的web客户端http请求工具,前者不再更新和维护;HttpClient常用于网络爬虫中,系统模拟浏览器发送请求获取数据;OkHttpClient高效低损

      • 使用:注册Bean到容器即可,如@Bean public RestTemplate restTemplate() { return new RestTemplate(); }

      • 请求及方法:

        • get请求无限制;post请求及方法:传递的参数如果用map类型,则必须是MultivalueMap

        • getForObject() / postForObject():获取对象;返回类型为对象

        • getForEntiry() / postForEntiry():获取对象,状态码,请求头信息等;返回类型为ResponseEntity封装的泛型对象

        // 注意上面说的Get或Post指的时url指向的第三方请求的请求方式
        public Map<String, object> getResult() {
            String url = "http://localhost:8080/add/...";
            return restTemplate.getForObject(url, Map.class); // 后面可以指定请求需要的参数列表
        }
         ​
        @GetMapping(...)
        public Map<String, object> getResult() {
            String url = "http://localhost:8080/add/...";
            Map<String, Object> params = new HashMap<>();
            ResponseEntity<HashMap> responseEntity = restTemplate.getForEntity(url, HashMap.class, params); // 后面可以指定请求需要的参数列表
            
            HttpStatus statusCode = responseEntity.getStatusCode(); // 状态码
            HttpHeaders headers = responseEntity.getHeraders(); // 请求头信息
            return responseEntity.getBody();
        }
         ​
        // 注意返回类型
        @GetMapping(...)
        public User getResult() {
            String url = "http://localhost:8080/add/...";
            MultivalueMap<String, Object> params = new LinkedMultivalueMap<>();
            params.put("age", 1);
            params.put("name", "MyName"); // 注意,使用了MultivalueMap,则add()方法中的参数接收不能用User对象,而应该用@ReqeustParam即具体的属性参数
            return restTemplate.postForObject(url, params, User.class);
            // 如果解析出来转换失败,可以使用ObjectMapper工具类
        }
        @GetMapping(...)
        public User getResult() {
            String url = "http://localhost:8080/add/..."; // 如果add()中参数接收用了对象,则需要写入请求体并设置请求头
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType("application/json");
            User user = new User(); // 当使用json数据传递的时候,封装为JSONObject即可
            user.setAge(age);
            user.setName(myName);
            HttpEntity<User> entityParam = new HttpEntity<User>(user, headers); // 写入
            return restTemplate.postForObject(url, entityParam, User.class); // 注意传参不同
        }

      • HttpClient应用之简单的网络爬虫(详情参见其他.md):

        import lombok.extern.slf4j.Slf4j;
        import org.apache.http.HttpEntity;
        import org.apache.http.client.methods.CloseableHttpResponse;
        import org.apache.http.client.methods.HttpGet;
        import org.apache.http.impl.client.CloseableHttpClient;
        import org.apache.http.impl.client.HttpClients;
        import org.apache.http.util.EntityUtils;
        ​
        import java.io.IOException;
        ​
        /**
         * 网络爬虫-模拟浏览器发送请求
         */
        @Slf4j
        public class HttpClientUtil {
            public static void main(String[] args) throws IOException {
                // 1、创建HttpClient实例,模拟打开浏览器
                CloseableHttpClient closeableHttpClient = HttpClients.createDefault();
                // 2、创建HttpGet实例;模拟发送请求一般都为get请求
                HttpGet httpGet = new HttpGet("http://www.baidu.com");
                // 3、执行get请求
                CloseableHttpResponse response = closeableHttpClient.execute(httpGet);
                // 4、解析响应
                if (200 == response.getStatusLine().getStatusCode()) {
                    HttpEntity entity = response.getEntity(); // 获取返回实体
                    String content = EntityUtils.toString(entity, "utf8");
                    log.info("获取请求响应数据:" + content); // 实体内容以字符串返回
                }
                response.close();
                closeableHttpClient.close();
            }
        }


2 模板引擎

2.1 Thymeleaf

  • 依赖基于starter启动器;源码类ThymeleafProperties

  • 语法:

/**
 * thymeleaf模板引擎用法:
 *  requestMapping:一般都为资源文件HTML名称
 *  引入th名称空间xmlns(xml namespace);HTML任意属性都可用th:替换
 *      特殊的标签内容:th:utext,它不转义特殊字符;如"<h1>内容</h1>"
 *          内容:用th:text得到并显示"<h1>内容</h1>",即<被转义,类似Java中的/后边的特殊字符被转义;用th:utext得到并显示"内容",即特殊字符<没被转义,解析为HTML了
 *          片段包含:th:insert,th:replace;
 *          遍历:th:each;
 *          条件判断:th:if;th:unless;th:switch;th:case;
 *           变量声明:th:object;th:with;
 *           属性修改:th:attr;th:attrprepend;th:attrappend;th:value;th:href;th:src;。。。。。。
 *           片段声明:th:fragment;
 *           移除:th:remove;
 *  配合表达式取值:
 *      ${...}:变量表达式
 *          获取变量值;调用方法;
 *          使用内置基本对象如#ctx,#vars,#locale,#request,#response,#session,#servletContext
 *          使用内置工具(类)对象如#strings,#numbers,#dates...等工具类
 *          另一种获取方式:[[]]或[()]转义字符行内转义,如<span th:text="${user"}></span> 等同于<span>[[${user}]]</span>
 *      *{...}:变量表达式:${...}的补充使用
 *          如${session.user}获取user,${session.user.name}获取user中的name
 *          而使用*{name}也可以获取user中的name(但注意作用域,在${session.user}所在标签及字标签内),即此时*等同于${session.user}
 *      #{...}:获取国际化内容
 *      @{...}:定义URL
 *          参数列表用小括号包裹,key-value形式,逗号隔开,value还可用变量表达式获取;若以/开头,表示路径为当前项目下
 *      ~{...}:片段引用表达式:尤其是引用公用的fragment
 *  字面量:字符串,数字,布尔值,null
 *  文本操作:字符串拼接:+;字符串替换:|...|
 *  数学运算:+,-,*,/,%
 *  布尔运算:and,or,!,not
 *  比较运算:>,<,==,!=等,及gt,lt,ge,le,eq,ne
 *  条件运算:if ? then :else;value ? defaultValue
 *
 */
@Controller
public class ThymeleafUsageController {
    // 相应处理:一种返回页面路径+名称,方法参数为Model,map,RedirectAttributes等类型
    @GetMapping("/prompt")
    public String promptSuccess(Map<String, Object> map) {
        map.put("text", "<h1>提示:成功!</h1>");
        map.put("utext", "<h1>提示:成功!</h1>");
        map.put("users", Arrays.asList("user1","user2","user3"));
        return "ymldefinetest";
    }
    // 另一种响应方式:HTML数据,即返回类型为ModelAndView或者Model,将页面路径+名称写入其中
​
    @GetMapping("/prompt2")
    public ModelAndView promptSuccess() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("text", "<h1>提示:成功!</h1>");
        modelAndView.addObject("utext", "<h1>提示:成功!</h1>");
        modelAndView.addObject("users", Arrays.asList("user1","user2","user3"));
        modelAndView.setViewName("ymldefinetest");
        return modelAndView;
    }
 ​
    /**
     * 与http请求和响应相关的数据
     * @param request
     * @param response
     */
    @RequestMapping("/http")
    public void http(HttpServletRequest request, HttpServletResponse response) {
        // request get...
        System.out.println(request.getMethod());
        System.out.println(request.getServletPath());
        Enumeration<String> headerNames = request.getHeaderNames();
​
        // response set...
        response.setContentType("text/html;charset=utf-8");
    }
}

3 myblog项目-未整理

  • 以后写项目,可以不用管前端了,直接百度bootstarap模板后导入

  • 项目功能规划:

    • 后端管理:管理员/用户登录,博客管理(增删查改),分类管理(增删查改),标签管理(增删查改)

    • 前端展示:首页(博客分页列表,top标签,top分类,最新推荐,博客详情),分类(分类列表,分类下的博客列表,博客详情),标签(标签列表,标签下的博客列表,博客详情),归档(按年度归档展示博客列表,博客详情)

  • 项目搭建:

    1. 选择Spring initializr,选择预先加载的依赖:DevTools热部署,Web(Spring web),thymeleaf,mysql,data jpa等

    2. 检查依赖是否正确;配置application.yml文件;检查单元测试(测试类中引入junit来做单元测试)

    3. 前端框架SemanticUI

    • 遇到的坑:

      1. MySQL的driver-class-name改了:com.mysql.cj.jdbc.Driver

      2. 启动SpringBoot报错MySQL存在问题:原因可能是前面选的MySQL下载的版本太高;需要在pom文件中指定version

      3. 启动SpringBoot报错Error creating bean with name 'inMemoryDatabaseShutdownExecutor:还不清楚错误原因;解决方案是在启动入口的@SpringBootApplication注解后面加上(exclude = {DataSourceAutoConfiguration.class })

    • 彩蛋-自定义启动时图像:网上搜索SpringBoot启动时Banner生成

    • 异常:

      • SpringBoot中有static和templates两个路径,分别用于存放静态页面和动态页面(模板如themeleaf等);静态页面可直接访问,动态页面则会请求后台;例如分别在其下建hello.html文件;输入localhost:8080/hello.html,则始终返回static下的文件;使用controller访问即输入localhost:8080/hello,static下也可访问

         @GetMapping("/hello")
         public String hello() {
             return "hello.htnl";
         }

        但templates下的则报错;原因:SpringBoot默认推荐thymeleaf,访问动态资源需要请求后台,即框架会分配一个ViewResolver去解析,因此需要书写controller,注意return中没有.html后缀;且访问的path不能与视图名重合,否则会抛出Circular view path异常,如此处不能写为@GetMapping("/hello")

         @GetMapping("/hi")
         public String hello() {
             return "hello";
         }
      • 记一个遇到的访问不了templates下文件的巨坑:页面一直报错:

        Writelabel Error Page

        This application has no explicit mapping for /error,so you are seeing this as a fallback.

        网上找了的很多方案(基本都是spring boot启动类因该放在controller外层的问题,于我不适用);最后发现代码中提示:Cannot resolve MVC View 'xxx';不能处理视图,那大概就是引入的thymeleaf依赖有冲突或不匹配;检查代码,引入的dependency没有问题,但这个地方的version和layout-dialect的version不匹配(下面是匹配的,不过我是直接将这两行代码删除了,可行)

         <properties>
             <java.version>1.8</java.version>
             <thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
             <!-- 布局功能的支持程序  thymeleaf3主程序  layout2以上版本 -->
             <!-- thymeleaf2   layout1-->
             <thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>
         </properties>
    • 定义错误页面、全局异常处理与日志处理:

      • 404/500:在templates下新建/error目录用于存储自定义错误页面,页面起名404.html/500.html,Springboot会在后台发生异常时去寻找这个目录,并跳转到对应页面

      • 自定义error:自定义标注为@ControllerAdvice的拦截器ExceptionHandler,拦截所有标注@Controller的控制器,使产生异常时跳转到自定义的error page

      • 自定义Exception:继承RuntimeException,此异常可由一些注解标注为何种异常,如@ResponseStatus(HttpStatus.Not_Found)标注为404;在出现异常的地方throw new MyException()

      • AOP方式以切面@Aspect拦截请求:配合@Pointcut,@Before,@After等,在日志输出一些信息如url,ip,args等,涉及的类HttpServletRequest;或者事务处理,缓存,权限校验等

    • 将写好的前端静态页面整合进来:因为项目使用的thymeleaf模板引擎(默认不支持JSP),需要修改适配:

      • fragment:将公用部分(包括JS)提取到模板,在需要的页面引用

      • 由此生成的网页都是动态网页,需要服务器启动后才能访问(直接浏览器打开可看到不起作用)

      • 一些前端插件:编辑器Markdown:推荐editor.md;内容排版typo.css:;动画animate.css:;代码高亮prism: ;滚动侦测waypoints:;平滑滚动:jquery.scrollTo:;目录生成Tocbot:;二维码生成qrcode.js

    • 生成实体类:

      • 使用jpa自动生成数据库表:(似乎需要配置idea到mysql的datasource????引入数据源,一直刷不出来数据表);曾经生成不了表,只能手动导入SQL

      • 记录一个巨坑:

        • 书写实体bean,并且@ManyToOne等一系列注解配置好数据表之间的关联关系后,启动springboot,正常情况是会在数据库中自动生成对应的数据表,包括中间表,有自联关系的,表字段也会体现出来;但是我的一直没有生成数据表,并且@Autowired注解标注的bean也在启动时报错找不到;查阅很多资料都说是路径问题导致扫描不到,但我确定我的路径没有问题。后来终于在网上找到答案:@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}):作用是排除自动注入数据源的配置(取消数据库配置)!!!!如果只需要简单的启动运行,则客家exclude,否则需要在配置文件中配置DataSource;因此不需要括号中的内容,于是数据表能够生成,且自动注入的bean也能找到了!汗!!!!!

    • dao-service-repository:

      • 各种数据操作CURD;定义Repository,方法命名严格遵守jpa规范;service使用CURD的结果作业务处理

      • 一个坑:查询时,jpa规范方法名findXXX()如findOne(id),但Springboot版本原因,可能的方法是其他如getOne(id)

    • controller:

      • 登录:用户合法验证,未登录用户拦截;输入非空验证;错误提示;密码MD5加密;登录成功与失败的页面跳转

      • 博客管理,分类管理,标签管理,归档管理,关于我,前端博客详情,评论回复;

      • 记一个坑:遇到这么个问题:解析三目运算符的时候,有id的(编辑)路径正常,没有id(即新增)的报错ip不存在

         <!--注意这里访问到的是controller中post方式的/types-->
         <!--此处先判断id是否存在,存在则调用controller中的编辑,否则为新增;id的获取使用一个隐藏的input-->
         <!--th:action="*{id}==null ? @{/admin/types} : @{/admin/types/{id}(id=*{id})}"-->
         <!--但是不知道为什么这个三目元算符运行时会报错;现在复制了一份文件为types-update,让controller去到不同的路径-->
         <form action="#" method="post" th:object="${type}" th:action="*{id}==null ? @{/admin/types} : @{/admin/types/{id}(id=*{id})}" class="ui form">
             <input type="hidden" name="id" th:value="*{id}">
             <div class="required field">
                 <div class="ui left labeled input">
                     <label class="ui teal basic label">名称</label>
                     <!--th:value="*{name}"在点击编辑的时候传递过来原始值并显示,其他时候为空,即新增-->
                     <input type="text" name="name" placeholder="分类名称" th:value="*{name}">
                 </div>
             </div>
             ......
      • 原先采用了复制一份页面分开写;后来找到原因:在Controller中,去到新增的请求中要实例化一个对象:

         // 点击新增,去到添加type的页面
         @GetMapping("/types/input")
         public String goToAddType(Model model) {
             model.addAttribute("type", new Type()); //这里需要添加这一行代码
             return "admin/types-input";
         }
         // ......
         // 点击编辑去到修改Type页面
         @GetMapping("/types/{id}/input")
         public  String editType(@PathVariable Long id, Model model) {
             model.addAttribute("type", typeService.queryType(id));
             return "admin/types-input";
         }
    • 项目打包与部署运行:

      • 右侧maven中package,日志中可看到XXX-0.0.1-SNAPSHOT.jar的打包路径(一般都在target目录下)

      • 复制此jar到任意目录下,运行命令java -jar jar包名来启动(当报错Unable to access jarfile FileEnDecrypt.jar时,可能环境变量配置有问题,jar包名使用绝对路径即可)


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值