spring boot全家桶新人入门手册

本文介绍了如何在SpringBoot项目中集成MyBatis,使用AOP进行日志打印和登录校验,以及分页、过滤器、拦截器、Redis缓存和WebSocket的使用。还讨论了事务处理和异常处理策略。
摘要由CSDN通过智能技术生成

0、写在最前面的话:jdk版本必须是8!!!jdk版本必须是8!!!jdk版本必须是8!!!

一、Spring Framework Runtime

Test

Spring提供的测试工具, 可以整合JUnit测试, 简化测试环节,对应spring-test.jar。

Core Container

Spring的核心组件, 包含了Spring框架最基本的支撑。

  • Beans:Spring 负责创建类对象并管理对象,对应spring-beans.jar。

  • core:核心类,对应spring-core.jar。

  • context:上下文参数,获取外部资源或管理注解,对应spring-context.jar。

  • SpEL:Spring表达式语言,对应spring-expression.jar。

spring启动需要的4个jar包。

  • Aop:面向切面编程, 对应spring-aop.jar。

  • Aspects:切面Aop依赖的包

  • Instrumentation:Spring 对服务器的代理接口

  • Messaging:信息体系结构和协议支持

Data Access/Integartion

Spring封装数据访问层相关内容

  • JDBC:Spring对jdbc的封装,当需要使用spring连接数据库时使用,对应spring-jdbc.jar.

  • ORM:spring整合第三方orm框架需要使用的jar包,例如Hibernate框架,对应spring-orm.jar。

  • OXM:Spring对于object/xml映射的支持,可以让JAVA与XML之间来回切换

  • JMS:为简化jms api的使用而做的简单封装

  • Transactions:用于事务管理,对应Spring-tx.jar。

Web

Spring完成web相关功能时需要。

spring boot运行

二、Spring中支持扩展的设计模式:

1、观察者模式在 Spring 中的应用



// Event事件
public class DemoEvent extends ApplicationEvent {
  private String message;

  public DemoEvent(Object source, String message) {
    super(source);
  }

  public String getMessage() {
    return this.message;
  }
}

// Listener监听者
@Component
public class DemoListener implements ApplicationListener<DemoEvent> {
  @Override
  public void onApplicationEvent(DemoEvent demoEvent) {
    String message = demoEvent.getMessage();
    System.out.println(message);
  }
}

// Publisher发送者
@Component
public class DemoPublisher {
  //自动装配,自动地为这个属性赋值
  @Autowired
  private ApplicationContext applicationContext;

  public void publishEvent(DemoEvent demoEvent) {
    this.applicationContext.publishEvent(demoEvent);
  }
}

@Autowired原理:
        举例:@Autowired
             private BookService bookService;
        1)、先按照类型去容器中找到对应的组件;bookService = ioc.getBean(BookService.class)
            ①、找到一个:找到就赋值
            ②、没找到就报异常
            ③、按照类型可以找到多个?找到多个如何装配上?
                a、类型一样就按照变量名为ID继续匹配
                  Ⅰ、匹配上就装配
                  Ⅱ、没有匹配?报错
                          原因:因为我们按照变量名作为id继续匹配的
                          使用@Qualifier指定一个新的id
                              找到就匹配

2、模板模式在 Spring 中的应用

① Spring Bean 的创建过程,可以大致分为两大步:对象的创建和对象的初始化。

② 对象的创建是通过反射来动态生成对象,而不是 new 方法。不管是哪种方式,说白了,总归还是调用构造函数来生成对象,没有什么特殊的。

模板模式在Spring Bean中创建使用示例:



public class DemoClass {
  //...
  
  public void initDemo() {
    //...初始化..
  }
}

// 配置:需要通过init-method显式地指定初始化方法
<bean id="demoBean" class="com.xzg.cd.DemoClass" init-method="initDemo"></bean>

这种初始化方式有一个缺点,初始化函数并不固定,由用户随意定义,这就需要 Spring 通过反射,在运行时动态地调用这个初始化函数。而反射又会影响代码执行的性能

③ Spring 针对对象的初始化过程,还做了进一步的细化,将它拆分成了三个小步骤:初始化前置操作、初始化、初始化后置操作。其中,中间的初始化操作就是我们刚刚讲的那部分,初始化的前置和后置操作,定义在接口 BeanPostProcessor 中。BeanPostProcessor 的接口定义如下所示:



public interface BeanPostProcessor {
  Object postProcessBeforeInitialization(Object var1, String var2) throws BeansException;

  Object postProcessAfterInitialization(Object var1, String var2) throws BeansException;
}

三、MyBatis如何权衡易用性、性能和灵活性?

1、Mybatis 和 ORM 框架介绍

① MyBatis 是一个 ORM(Object Relational Mapping,对象 - 关系映射)框架

② MyBatis 依赖 JDBC 驱动,所以,在项目中使用 MyBatis,除了需要引入 MyBatis 框架本身(mybatis.jar)之外,还需要引入 JDBC 驱动(比如,访问 MySQL 的 JDBC 驱动实现类库 mysql-connector-java.jar)。将两个 jar 包引入项目之后,我们就可以开始编程了。使用 MyBatis 来访问数据库中用户信息的代码如下所示:



// 1. 定义UserDO
public class UserDo {
  private long id;
  private String name;
  private String telephone;
  // 省略setter/getter方法
}

// 2. 定义访问接口
public interface UserMapper {
  public UserDo selectById(long id);
}

// 3. 定义映射关系:UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.xzg.cd.a87.repo.mapper.UserMapper">
    <select id="selectById" resultType="cn.xzg.cd.a87.repo.UserDo">
        select * from user where id=#{id}
    </select>
</mapper>

// 4. 全局配置文件: mybatis.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="dev">
        <environment id="dev">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8" />
                <property name="username" value="root" />
                <property name="password" value="..." />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

在使用 MyBatis 的实现方式中,类字段与数据库字段之间的映射关系、接口与 SQL 之间的映射关系,是写在 XML 配置文件中的,是跟代码相分离的,这样会更加灵活、清晰,维护起来更加方便。

2、如何利用职责链与代理模式实现MyBatis Plugin?

例如:假设我们需要统计应用中每个 SQL 的执行耗时,如果使用 MyBatis Plugin 来实现的话,我们只需要定义一个 SqlCostTimeInterceptor 类,让它实现 MyBatis 的 Interceptor 接口,并且,在 MyBatis 的全局配置文件中,简单声明一下这个插件就可以了。具体的代码和配置如下所示



@Intercepts({
        @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})
public class SqlCostTimeInterceptor implements Interceptor {
  private static Logger logger = LoggerFactory.getLogger(SqlCostTimeInterceptor.class);

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Object target = invocation.getTarget();
    long startTime = System.currentTimeMillis();
    StatementHandler statementHandler = (StatementHandler) target;
    try {
      return invocation.proceed();
    } finally {
      long costTime = System.currentTimeMillis() - startTime;
      BoundSql boundSql = statementHandler.getBoundSql();
      String sql = boundSql.getSql();
      logger.info("执行 SQL:[ {} ]执行耗时[ {} ms]", sql, costTime);
    }
  }

  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
    System.out.println("插件配置的信息:"+properties);
  }
}

<!-- MyBatis全局配置文件:mybatis-config.xml -->
<plugins>
  <plugin interceptor="com.xzg.cd.a88.SqlCostTimeInterceptor">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

我们只重点看下 @Intercepts 注解这一部分。

我们知道,不管是拦截器、过滤器还是插件,都需要明确地标明拦截的目标方法。

@Intercepts 注解实际上就是起了这个作用。其中,@Intercepts 注解又可以嵌套 @Signature 注解。一个 @Signature 注解标明一个要拦截的目标方法。如果要拦截多个方法,我们可以像例子中那样,编写多条 @Signature 注解。

@Signature 注解包含三个元素:type、method、args。其中,type 指明要拦截的类、method 指明方法名、args 指明方法的参数列表。通过指定这三个元素,我们就能完全确定一个要拦截的方法。

默认情况下,MyBatis Plugin 允许拦截的方法有下面这样几个:

四、Spring使用规则:

0、开发顺序:先写Dao -> Service -> Controller

        单元测试:Service

        调试Dao层:Mybatis打印sql

1、请求方法设置

@RestController:用来声明一个 Controller 类,加载到 Spring Boot 上下文;

@RequestMapping:指定当前类中所有方法在 URL 中的访问路径的前缀;

@Post/Get/PutMapping:定义当前方法的 HTTP Method 和访问路径。

@RestController类中的所有方法只能返回String、Object、Json等实体对象,不能跳转到模版页面。

@RequestMapping是一个用来处理请求地址映射的注解,表示支持GET、POST、PUT、DELETE所有请求

@GetMapping、@PostMapping、@PutMapping、@DeleteMapping

@RequestMapping(value = "/user/1",method = RequestMethod.GET)

表示get请求用户id等于1

controller/TestController.java


package com.dukaiwen.wiki.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @RequestMapping("/hello")
    public  String hello(){
        return "Hello World!";
    }
}

2、请求状态type=Method Not Allowed, status=405表示没有这个接口

3、@SpringBootApplication注解一般放在项目的一个启动类上,用来把启动类注入到容器中,用来定义容器扫描的范围,用来加载classpath环境中一些bean。

@SpringBootApplication 注解会自动开启包路径扫描,并启动一系列的自动装配流程(AutoConfig)。

如果我们想加载额外的扫包路径,只用添加 ComponentScan 注解并指定 path 即可。

4、使用@ComponentScan来扫描包(限制在同一个包内才能扫描到,不同包可以使用@ComponentScan({"com.dukaiwen","com.test"})),让我们直接创建,无需以自己引入

5、波浪文字处理a|t+Enter实现添加到字典中

6、配置端口号:在resources中application.properties文件中配置端口号


server.port = 8880

系统识别端口号位置:resources中config文件下application.properties、application.yml也可以识别,注意:单个SpringBoot不会读bootstrap配置,要SpringCloud架构下的SpringBoot应用才会读,bootstrap一般用于动态配置,线上可以实时修改实时生效的配置,一般可配合上nacos配置中心使用

7、全局配置文件application.properties中


server.port=8880
test.hello=Hello

用@Value("${test.hello:TEST}"),冒号后面默认值(优先读配置文件,没有就用默认值),防止生产配有配置值导致服务起不来问题

controller/TestController.java


    @Value("${test.hello:TEST}")
    private String testHello;

    @GetMapping("/hello")
    public  String hello(){
        return "Hello World!" + testHello;
    }

8、集成热部署【点击小锤子🔨】

在pom.xml中添加依赖

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

补充知识点:内部可以不用加版本号,因为有父项目中版本

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

9、Mybatis

依赖引入

pom.xml

        <!-- 集成mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <!-- 集成mysql连接 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>

配置数据源

application.properties


spring.datasource.url=jdbc:mysql://rm-uf6470s9615e13hc4no.mysql.rds.aliyuncs.com/wikidev?characterEncoding=UTF8&autoReconnect=true&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.datasource.username=wikidev
spring.datasource.password=wikidevABC123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

10、数据表对象层

domain,或entity,或POJO,总之这一层实体类就是和数据库表一一映射

domain/Test.java


package com.jiawa.wiki.domain;

public class Test {

    private Integer id;

    private String name;

    private String password;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

11、持久层叫Mapper层,即广为人知的Dao层。因为后续要用官方代码生成器,其生成的代码就是XXXMapper

mapper/TestMapper.java(interface)


package com.jiawa.wiki.mapper;

import com.jiawa.wiki.domain.Test;

import java.util.List;

public interface TestMapper {

    public List<Test> list();
}

12、sql脚本存放位置

resources/mapper/TestMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.jiawa.wiki.mapper.TestMapper" >

    <select id="list" resultType="com.jiawa.wiki.domain.Test">
        select `id`, `name`, `password` from `test`
    </select>

</mapper>

1)<mapper namespace="com.jiawa.wiki.mapper.TestMapper" >中namespace对应的mapper/TestMapper.java(interface)文件的package名称+interface名称

2)<select id="list" resultType="com.jiawa.wiki.domain.Test">中id对应的mapper/TestMapper.java(interface)文件的public List<Test> list()的list方法

3)<mapper namespace="com.jiawa.wiki.mapper.TestMapper" >对应的是domain中的实体类

4) select `id`, `name`, `password` from `test`就是sql语法

13、添加mapper扫描注解

在config/WikiApplication.java


@MapperScan("com.jiawa.wiki.mapper")

1)对应的是mapper/TestMapper.java(interface)文件的package名称

14、配置mybatis所有Mapper.xml所在的路径

在application.properties中


# 配置mybatis所有Mapper.xml所在的路径
mybatis.mapper-locations=classpath:/mapper/**/*.xml

15、Service层:所有逻辑都放在Service

service/TestService.java


package com.jiawa.wiki.service;

import com.jiawa.wiki.domain.Test;
import com.jiawa.wiki.mapper.TestMapper;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class TestService1 {

    @Resource
    private TestMapper testMapper;

    public List<Test> list(){
        return testMapper.list();
    }
}

使用@Service注解,将这个Service交给Spring来管理

使用@Resource注解,将TestMapper数据源引入

自定义一个公共方法list返回数据

16、controller层中新增一个

controller/TestController.java


    @Resource
    private TestService testService;

    @GetMapping("/test/list")
    public List<Test> list(){
        return testService.list();
    }

@getMapping设置路由

@Resource返回service

17、打包工具依赖

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

18、mybatis-generator依赖引入

            <!-- mybatis generator 自动生成代码插件 -->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.0</version>
                <configuration>
                    <configurationFile>src/main/resources/generator/generator-config.xml</configurationFile>
                    <overwrite>true</overwrite>
                    <verbose>true</verbose>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.22</version>
                    </dependency>
                </dependencies>
            </plugin>    

1)新建配置文件

/resources/generator/generator-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">

        <!-- 自动检查关键字,为关键字增加反引号 -->
        <property name="autoDelimitKeywords" value="true"/>
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!--覆盖生成XML文件-->
        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
        <!-- 生成的实体类添加toString()方法 -->
        <plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>

        <!-- 不生成注释 -->
        <commentGenerator>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://rm-uf6470s9615e13hc4no.mysql.rds.aliyuncs.com:3306/wikidev?serverTimezone=Asia/Shanghai"
                        userId="wikidev"
                        password="wikidevABC123">
        </jdbcConnection>

        <!-- domain类的位置 -->
        <javaModelGenerator targetProject="src\main\java"
                            targetPackage="com.jiawa.wiki.domain"/>

        <!-- mapper xml的位置 -->
        <sqlMapGenerator targetProject="src\main\resources"
                         targetPackage="mapper"/>

        <!-- mapper类的位置 -->
        <javaClientGenerator targetProject="src\main\java"
                             targetPackage="com.jiawa.wiki.mapper"
                             type="XMLMAPPER"/>

        <!--<table tableName="demo" domainObjectName="Demo"/>-->
        <!--<table tableName="ebook"/>-->
        <!--<table tableName="category"/>-->
        <!--<table tableName="doc"/>-->
        <!--<table tableName="content"/>-->
        <!--<table tableName="user"/>-->
        <table tableName="ebook_snapshot"/>
    </context>
</generatorConfiguration>

修改数据库连接

/resources/generator/generator-config.xml

<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://rm-uf6470s9615e13hc4no.mysql.rds.aliyuncs.com:3306/wikidev?serverTimezone=Asia/Shanghai"
                        userId="wikidev"
                        password="wikidevABC123">
        </jdbcConnection>

对某张表进行生成数据持久层

/resources/generator/generator-config.xml

<table tableName="ebook_snapshot"/>

调用方法

service/TestService


    @Resource
    private TestMapper testMapper;

    public List<Test> list() {
        return testMapper.selectByExample(new TestExample());
    }

19、Service、controller层修改内容

比如直接替换文件中Demo、demo替换【选中Match Case、不要选中Words】

20、controller请求配置(类似配置父子路由)

controller/DemoController.java


@RestController
@RequestMapping("/demo")
public class DemoController {

    @Resource
    private DemoService demoService;

    @GetMapping("/list")
    public List<Demo> list() {
        return demoService.list();
    }
}

21、myBatis-generator配置

1)pom.xml

<!-- mybatis generator 自动生成代码插件 -->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.0</version>
                <configuration>
                    <configurationFile>src/main/resources/generator/generator-config.xml</configurationFile>
                    <overwrite>true</overwrite>
                    <verbose>true</verbose>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.22</version>
                    </dependency>
                </dependencies>
            </plugin>

2)在resources/generator新建generator-config.xml【配置不同的表生成不同的文件】

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">

        <!-- 自动检查关键字,为关键字增加反引号 -->
        <property name="autoDelimitKeywords" value="true"/>
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!--覆盖生成XML文件-->
        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
        <!-- 生成的实体类添加toString()方法 -->
        <plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>

        <!-- 不生成注释 -->
        <commentGenerator>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://rm-uf6470s9615e13hc4no.mysql.rds.aliyuncs.com:3306/wikidev?serverTimezone=Asia/Shanghai"
                        userId="wikidev"
                        password="wikidevABC123">
        </jdbcConnection>

        <!-- domain类的位置 -->
        <javaModelGenerator targetProject="src\main\java"
                            targetPackage="com.jiawa.wiki.domain"/>

        <!-- mapper xml的位置 -->
        <sqlMapGenerator targetProject="src\main\resources"
                         targetPackage="mapper"/>

        <!-- mapper类的位置 -->
        <javaClientGenerator targetProject="src\main\java"
                             targetPackage="com.jiawa.wiki.mapper"
                             type="XMLMAPPER"/>

        <!--<table tableName="demo" domainObjectName="Demo"/>-->
        <table tableName="ebook"/>
        <!--<table tableName="category"/>-->
        <!--<table tableName="doc"/>-->
        <!--<table tableName="content"/>-->
        <!--<table tableName="user"/>-->
<!--        <table tableName="ebook_snapshot"/>-->
    </context>
</generatorConfiguration>

3)在Idea中新增配置

22、返回格式统一

1)新建resp/CommonResp.java


package com.jiawa.wiki.resp;

public class CommonResp<T> {

    /**
     * 业务上的成功或失败
     */
    private boolean success = true;

    /**
     * 返回信息
     */
    private String message;

    /**
     * 返回泛型数据,自定义类型
     */
    private T content;

    public boolean getSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getContent() {
        return content;
    }

    public void setContent(T content) {
        this.content = content;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("ResponseDto{");
        sb.append("success=").append(success);
        sb.append(", message='").append(message).append('\'');
        sb.append(", content=").append(content);
        sb.append('}');
        return sb.toString();
    }
}

2)使用示例

controller/EbookController.java


    @GetMapping("/list")
    public CommonResp list(String name) {
        CommonResp<List<Ebook>> resp = new CommonResp<>();
        List<Ebook> list = ebookService.list(name);
        resp.setContent(list);
        return resp;
    }

service/EbookService.java【Criteria:相当与Where条件】


    public List<Ebook> list(String name){
        EbookExample ebookExample = new EbookExample();
        EbookExample.Criteria criteria = ebookExample.createCriteria();
        criteria.andNameLike("%"+name+"%");
        return ebookMapper.selectByExample(ebookExample);
    }

23、快速生成一个变量


Ctrl+A|t+v

24、封装请求参数

注意:①将请求所有参数封装成一个类

②toString主要是用来打日志

1)新建req/EbookQueryReq.java


package com.jiawa.wiki.req;

public class EbookQueryReq {
    private Long id;

    private String name;

    private Long categoryId2;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getCategoryId2() {
        return categoryId2;
    }

    public void setCategoryId2(Long categoryId2) {
        this.categoryId2 = categoryId2;
    }

    @Override
    public String toString() {
        return "EbookQueryReq{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", categoryId2=" + categoryId2 +
                "} " + super.toString();
    }
}

2)Controller层示例

EbookController.java


    @GetMapping("/list")
    public CommonResp list(EbookQueryReq req) {
        CommonResp<List<Ebook>> resp = new CommonResp<>();
        List<Ebook> list = ebookService.list(req);
        resp.setContent(list);
        return resp;
    }

3)Service层示例

EbookService.java【Spring会自动将参数映射到类属性】


    public List<Ebook> list(EbookQueryReq req){
        EbookExample ebookExample = new EbookExample();
        EbookExample.Criteria criteria = ebookExample.createCriteria();
        criteria.andNameLike("%"+req.getName()+"%");
        return ebookMapper.selectByExample(ebookExample);
    }

25、封装返回参数

1)新建resq/EbookQueryResp.java


package com.jiawa.wiki.resp;

public class EbookQueryResp {
    private Long id;

    private String name;

    private Long category1Id;

    private Long category2Id;

    private String description;

    private String cover;

    private Integer docCount;

    private Integer viewCount;

    private Integer voteCount;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getCategory1Id() {
        return category1Id;
    }

    public void setCategory1Id(Long category1Id) {
        this.category1Id = category1Id;
    }

    public Long getCategory2Id() {
        return category2Id;
    }

    public void setCategory2Id(Long category2Id) {
        this.category2Id = category2Id;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getCover() {
        return cover;
    }

    public void setCover(String cover) {
        this.cover = cover;
    }

    public Integer getDocCount() {
        return docCount;
    }

    public void setDocCount(Integer docCount) {
        this.docCount = docCount;
    }

    public Integer getViewCount() {
        return viewCount;
    }

    public void setViewCount(Integer viewCount) {
        this.viewCount = viewCount;
    }

    public Integer getVoteCount() {
        return voteCount;
    }

    public void setVoteCount(Integer voteCount) {
        this.voteCount = voteCount;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getSimpleName());
        sb.append(" [");
        sb.append("Hash = ").append(hashCode());
        sb.append(", id=").append(id);
        sb.append(", name=").append(name);
        sb.append(", category1Id=").append(category1Id);
        sb.append(", category2Id=").append(category2Id);
        sb.append(", description=").append(description);
        sb.append(", cover=").append(cover);
        sb.append(", docCount=").append(docCount);
        sb.append(", viewCount=").append(viewCount);
        sb.append(", voteCount=").append(voteCount);
        sb.append("]");
        return sb.toString();
    }
}

2)Service层示例

service/EbookService.java【Spring会自动将参数映射到类属性】


     public List<EbookQueryResp> list(EbookQueryReq req){
        EbookExample ebookExample = new EbookExample();
        EbookExample.Criteria criteria = ebookExample.createCriteria();
        criteria.andNameLike("%"+req.getName()+"%");
        List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);
        //  持久层返回List<Ebook>需要转成List<EbookResp>再返回controller
        List<EbookQueryResp> respList = new ArrayList<>();
        for (Ebook ebook : ebookList) {
            EbookQueryResp ebookQueryResp = new EbookQueryResp();
//            通过spring工具方法实现类的拷贝
            BeanUtils.copyProperties(ebook, ebookQueryResp);
            respList.add(ebookQueryResp);
        }
        return respList;
    }

3)Controller层示例

EbookController.java【在controller层不要出现domain实体ebook】


    @GetMapping("/list")
    public CommonResp list(EbookQueryReq req) {
        CommonResp<List<EbookQueryResp>> resp = new CommonResp<>();
        List<EbookQueryResp> list = ebookService.list(req);
        resp.setContent(list);
        return resp;
    }

26、制作CopyUtil封装BeanUtils

1)新增util/CopyUtil.js


package com.jiawa.wiki.util;

import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;

public class CopyUtil {

    /**
     * 单体复制
     */
    public static <T> T copy(Object source, Class<T> clazz) {
        if (source == null) {
            return null;
        }
        T obj = null;
        try {
            obj = clazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        BeanUtils.copyProperties(source, obj);
        return obj;
    }

    /**
     * 列表复制
     */
    public static <T> List<T> copyList(List source, Class<T> clazz) {
        List<T> target = new ArrayList<>();
        if (!CollectionUtils.isEmpty(source)){
            for (Object c: source) {
                T obj = copy(c, clazz);
                target.add(obj);
            }
        }
        return target;
    }
}

2)使用

单体复制用法

EbookController.java


        CopyUtil.copy(ebook, EbookQueryResp.class);

列表复制用法

EbookController.java


        List<EbookQueryResp> respList = CopyUtil.copyList(ebookList, EbookQueryResp.class);
        return respList;

27、编译报错问题处理【maven中Lifecycle中点击clean】,完成后再编译

28、解决跨域问题

1)新建config/CorsConfig.java


package com.jiawa.wiki.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedHeaders(CorsConfiguration.ALL)
                .allowedMethods(CorsConfiguration.ALL)
                .allowCredentials(true)
                .maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求)
    }

}

29、动态sql处理

service/EbookService.java


        if(!ObjectUtils.isEmpty(req.getName())){
            criteria.andNameLike("%"+req.getName()+"%");
        }

30、SpringBoot过滤器的使用

filter/LogFilter.java【增加@Component这个注解,Spring就会去扫描。我们的容器就会拿到这个过滤器】


 package com.jiawa.wiki.filter;

 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;

 import javax.servlet.*;
 import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;

 @Component
 public class LogFilter implements Filter {

     private static final Logger LOG = LoggerFactory.getLogger(LogFilter.class);

     @Override
     public void init(FilterConfig filterConfig) throws ServletException {

     }

     @Override
     public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
         // 打印请求信息
         HttpServletRequest request = (HttpServletRequest) servletRequest;
         LOG.info("------------- LogFilter 开始 -------------");
         LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
         LOG.info("远程地址: {}", request.getRemoteAddr());

         long startTime = System.currentTimeMillis();
         filterChain.doFilter(servletRequest, servletResponse);
         LOG.info("------------- LogFilter 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
     }
 }

注:filterChain.doFilter(servletRequest, servletResponse);类似await

31、格式化

Ctrl+A|t+L

32、日志折行

33、SpringBoot拦截器的使用【和拦截器不太一样,它前面和后面是分开的,调用业务方法是自己去写】

1)过滤器【拦截器是Spring框架特有的,常用于登录校验,权限校验,请求日志打印】

interceptor/LoginInterceptor.java


package com.jiawa.wiki.interceptor;

import com.alibaba.fastjson.JSON;
import com.jiawa.wiki.resp.UserLoginResp;
import com.jiawa.wiki.util.LoginUserContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class);

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 打印请求信息
        LOG.info("------------- LoginInterceptor 开始 -------------");
        long startTime = System.currentTimeMillis();
        request.setAttribute("requestStartTime", startTime);

        // OPTIONS请求不做校验,
        // 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验
        if(request.getMethod().toUpperCase().equals("OPTIONS")){
            return true;
        }

        String path = request.getRequestURL().toString();
        LOG.info("接口登录拦截:,path:{}", path);

        //获取header的token参数
        String token = request.getHeader("token");
        LOG.info("登录校验开始,token:{}", token);
        if (token == null || token.isEmpty()) {
            LOG.info( "token为空,请求被拦截" );
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
        Object object = redisTemplate.opsForValue().get(token);
        if (object == null) {
            LOG.warn( "token无效,请求被拦截" );
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        } else {
            LOG.info("已登录:{}", object);
            LoginUserContext.setUser(JSON.parseObject((String) object, UserLoginResp.class));
            return true;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        long startTime = (Long) request.getAttribute("requestStartTime");
        LOG.info("------------- LoginInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        LOG.info("LogInterceptor 结束");
    }
}

注:preHandle返回true往后走,返回false结束

2)过滤器配置

config/SpringMvcConfig.java


@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

    @Resource
    LoginInterceptor loginInterceptor;

    @Resource
    ActionInterceptor actionInterceptor;

    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/test/**",
                        "/redis/**",
                        "/user/login",
                        "/category/all",
                        "/ebook/list",
                        "/doc/all/**",
                        "/doc/vote/**",
                        "/doc/find-content/**",
                        "/ebook-snapshot/**",
                        "/ebook/upload/avatar",
                        "/file/**"
                );

        registry.addInterceptor(actionInterceptor)
                .addPathPatterns(
                        "/*/save",
                        "/*/delete/**",
                        "/*/reset-password");
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/file/**").addResourceLocations("file:D:/file/wiki/");
    }
}

34、系统流程

过滤器范围更大,过滤器在容器tomcat中,接口一进来会先到容器 ——>然后容器会发到应用

我们的SpringBoot它就是一个应用,一个web应用,所以再进到web应用我们的拦截器就拿到了

,再执行业务逻辑,最后拦截器先结束,然后过滤器再结束

35、SpringBootAOP的使用

配置AOP,打印接口耗时、请求参数、返回参数

先加依赖

pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.70</version>
        </dependency>

新增aspect/LogAspect.java


package com.jiawa.wiki.aspect;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import com.jiawa.wiki.util.RequestContext;
import com.jiawa.wiki.util.SnowFlake;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

@Aspect
@Component
public class LogAspect {

    private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);

    /** 定义一个切点 */
    @Pointcut("execution(public * com.jiawa.*.controller..*Controller.*(..))")
    public void controllerPointcut() {}

    @Resource
    private SnowFlake snowFlake;

    @Before("controllerPointcut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {

        // 增加日志流水号
        MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));

        // 开始打印请求日志
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();

        // 打印请求信息
        LOG.info("------------- 开始 -------------");
        LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
        LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
        LOG.info("远程地址: {}", request.getRemoteAddr());

        RequestContext.setRemoteAddr(getRemoteIp(request));

        // 打印请求参数
        Object[] args = joinPoint.getArgs();
        // LOG.info("请求参数: {}", JSONObject.toJSONString(args));

        Object[] arguments  = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof ServletRequest
                    || args[i] instanceof ServletResponse
                    || args[i] instanceof MultipartFile) {
                continue;
            }
            arguments[i] = args[i];
        }
        // 排除字段,敏感字段或太长的字段不显示
        String[] excludeProperties = {"password", "file"};
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
        excludefilter.addExcludes(excludeProperties);
        LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
    }

    @Around("controllerPointcut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        // 排除字段,敏感字段或太长的字段不显示
        String[] excludeProperties = {"password", "file"};
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
        excludefilter.addExcludes(excludeProperties);
        LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
        LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
        return result;
    }

    /**
     * 使用nginx做反向代理,需要用该方法才能取到真实的远程IP
     * @param request
     * @return
     */
    public String getRemoteIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

}


    public void pointcut() {
    }

    @Around(value = "pointcut()")
    public Object dealHandler(ProceedingJoinPoint point) throws Throwable {

        Object[] args = point.getArgs();

        long startTime = System.currentTimeMillis();
        String methodName = point.getSignature().getName();
        String className = point.getTarget().getClass().getSimpleName();


        LoggerClient.info("start {}.{} request , arg:{}", className, methodName, JSONObject.toJSONString(args));

        try {
            Object result = point.proceed();

            long endTime = System.currentTimeMillis();

            LoggerClient.info("finish {}.{} request, result:{} cost {}ms", className, methodName, JSONObject.toJSONString(result), (endTime - startTime));
            return result;
        } catch (Exception e) {
            long endTime = System.currentTimeMillis();
            LoggerClient.info("finish {}.{} request, but an exception occurs, exception:{}, cost {}ms", className, methodName, Throwables.getStackTraceAsString(e), (endTime - startTime));

            throw e;
        }
    }

注意:

1)使用AOP要用@Aspect注解、@component表示你要把这个类交给Spring来管理

2)切点:@Pointcut,针对所有的controller都进到这个方法

3)前置通知:@Before,就是执行业务代码之前,我们要去做的事情,就是去打印日志

4)连接点:joinPoint,通过连接点拿到入参,然后将入参打印

5)排除字段:敏感字段或太长的字段不显示

6)环绕:@Around,围绕业务前后执行,打印开始时间、过滤内容、返回结果、耗时

7)执行内容:Object result = proceedingJoinPoint.proceed();

8)aspect切面:切点@Pointcut+通知@Before、@Around、@After

36、使用PageHelper实现前后端分页【帮我们实现了sql中查总数count+查当前页数据limit】

pom.xml

<!-- pagehelper 插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.13</version>
        </dependency>

使用

service/EbookService.java


public List<EbookQueryResp> list(EbookQueryReq req){
        EbookExample ebookExample = new EbookExample();
        EbookExample.Criteria criteria = ebookExample.createCriteria();
        if(!ObjectUtils.isEmpty(req.getName())){
            criteria.andNameLike("%"+req.getName()+"%");
        }
        // 分页
        PageHelper.startPage(1,3);
        List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);

        PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
        LOG.info("总行数"+pageInfo.getTotal());
        LOG.info("总页数"+pageInfo.getPages());

        List<EbookQueryResp> respList = CopyUtil.copyList(ebookList, EbookQueryResp.class);
        return respList;
    }

注意:分页必须放在查询语句的下面一行,防止分页作用于其他查询语句导致本条查询分页失效

37、打印数据库的日志

application.properties


# 打印所有的sql日志:sql, 参数, 结果
logging.level.com.jiawa.wiki.mapper=trace

38、封装分页请求参数

req/PageReq.java


package com.jiawa.wiki.req;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;

public class PageReq {
    @NotNull(message = "【页码】不能为空")
    private int page;

    @NotNull(message = "【每页条数】不能为空")
    @Max(value = 1000, message = "【每页条数】不能超过1000")
    private int size;

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("PageReq{");
        sb.append("page=").append(page);
        sb.append(", size=").append(size);
        sb.append('}');
        return sb.toString();
    }
}

使用

EbookQueryReq.java


public class EbookQueryReq extends PageReq {
}

使用

EbookService.java


PageHelper.startPage(req.getPage(), req.getSize());

39、封装分页返回参数

补充泛型使用:不确定类型使用泛型

resp/CommonResp


public class CommonResp<T> {
    /**
     * 返回泛型数据,自定义类型
     */
    private T content;
}

封装返回参数:

resp/PageResp.java


package com.jiawa.wiki.resp;

import java.util.List;

public class PageResp<T> {
    private long total;

    private List<T> list;

    public long getTotal() {
        return total;
    }

    public void setTotal(long total) {
        this.total = total;
    }

    public List<T> getList() {
        return list;
    }

    public void setList(List<T> list) {
        this.list = list;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("PageResp{");
        sb.append("total=").append(total);
        sb.append(", list=").append(list);
        sb.append('}');
        return sb.toString();
    }
}

使用

service/EbookService.java


 public PageResp<EbookQueryResp> list(EbookQueryReq req){
        EbookExample ebookExample = new EbookExample();
        EbookExample.Criteria criteria = ebookExample.createCriteria();
        if(!ObjectUtils.isEmpty(req.getName())){
            criteria.andNameLike("%"+req.getName()+"%");
        }
        PageHelper.startPage(req.getPage(), req.getSize());
        List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);

        PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
        LOG.info("总行数"+pageInfo.getTotal());
        LOG.info("总页数"+pageInfo.getPages());

        // 列表复制
        List<EbookQueryResp> respList = CopyUtil.copyList(ebookList, EbookQueryResp.class);
        
        PageResp<EbookQueryResp> pageResp = new PageResp();
        pageResp.setTotal(pageInfo.getTotal());
        pageResp.setList(respList);

        return pageResp;
    }

controller/EbookController.java


    @GetMapping("/list")
    public CommonResp list(EbookQueryReq req) {
        CommonResp<PageResp<EbookQueryResp>> resp = new CommonResp<>();
        PageResp<EbookQueryResp> list = ebookService.list(req);
        resp.setContent(list);
        return resp;
    }

40、保存功能

controller/EbookController.java


    @PostMapping("/save")
    public CommonResp save(@Valid @RequestBody EbookSaveReq req){
        CommonResp resp = new CommonResp<>();
        ebookService.save(req);
        return resp;
    }

注意:希望通过注解校验post请求的body,需要用到@Valid注解

在request实体类添加注解进行校验,例如用@NotNull进行判空校验

例如:


@Data
public class MyRequest {

    @NotNull(message = "id 不能为空")
    private Integer id;

    @NotNull(message = "name 不能为空")
    private String name;
}

补充:

@Data : 注解在类上, 为类提供读写属性, 此外还提供了 equals()、hashCode()、toString() 方法

@Getter/@Setter : 注解在类上, 为类提供读写属性

@ToString : 注解在类上, 为类提供 toString() 方法

@Slf4j : 注解在类上, 为类提供一个属性名为 log 的 log4j 的日志对象

@Log4j : 注解在类上, 为类提供一个属性名为 log 的 log4j 的日志对象

service/EbookService.java


/**
     * 保存
     * @param req
     */
    public void save(EbookSaveReq req){
        Ebook ebook = CopyUtil.copy(req,Ebook.class);
        if(ObjectUtils.isEmpty(req.getId())){
            // 新增
            ebookMapper.insert(ebook);
        }else{
            // 更新
            ebookMapper.updateByPrimaryKey(ebook);
        }
    }

41、雪花算法精度丢失问题

注意!注意!注意!雪花算法ID是Long类型,前端是number类型,精度上不一样,Long类型超过一定长度后,前端接收到的值会不准确。会导致新增的记录不能编辑、删除。

日期获取时间戳


         String dateTime = "2021-01-01 08:00:00";
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
         System.out.println(sdf.parse(dateTime).getTime());

util/SnowFlake


@Component
public class SnowFlake { ... }

后期使用通过注入写法,所以用@Component将SnowFlake交给spring来管理

使用方法:@Resource是jdk自带的,@Autowired是Spring自带的

service/EbookService


    @Resource
    private SnowFlake snowFlake;

    /**
     * 保存
     * @param req
     */
    public void save(EbookSaveReq req){
        Ebook ebook = CopyUtil.copy(req,Ebook.class);
        if(ObjectUtils.isEmpty(req.getId())){
            // 新增
            ebook.setId(snowFlake.nextId());
            ebookMapper.insert(ebook);
        }else{
            // 更新
            ebookMapper.updateByPrimaryKey(ebook);
        }
    }

42、删除功能

controller/EbookController


    @DeleteMapping("/delete/{id}")
    public CommonResp delete(@PathVariable Long id){
        CommonResp resp = new CommonResp<>();
        ebookService.delete(id);
        return resp;
    }

service/EbookService


    public void delete(Long id){
        ebookMapper.deleteByPrimaryKey(id);
    }

43、集成Validation做参数校验

pom.xml

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

使用

req/PageReq


public class PageReq {
    @NotNull(message = "【页码】不能为空")
    private int page;

    @NotNull(message = "【每页条数】不能为空")
    @Max(value = 1000, message = "【每页条数】不能超过1000")
    private int size;
}

使用@Valid 开启校验规则

controller/EbookController


    @GetMapping("/list")
    public CommonResp list(@Valid EbookQueryReq req) {
        CommonResp<PageResp<EbookQueryResp>> resp = new CommonResp<>();
        PageResp<EbookQueryResp> list = ebookService.list(req);
        resp.setContent(list);
        return resp;
    }

44、统一异常处理

controller/ControllerExceptionHandler


package com.jiawa.wiki.controller;

import com.jiawa.wiki.exception.BusinessException;
import com.jiawa.wiki.resp.CommonResp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 统一异常处理、数据预处理等
 */
@ControllerAdvice
public class ControllerExceptionHandler {

    private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);

    /**
     * 校验异常统一处理
     * @param e
     * @return
     */
    @ExceptionHandler(value = BindException.class)
    @ResponseBody
    public CommonResp validExceptionHandler(BindException e) {
        CommonResp commonResp = new CommonResp();
        LOG.warn("参数校验失败:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        commonResp.setSuccess(false);
        commonResp.setMessage(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return commonResp;
    }

    /**
     * 校验异常统一处理
     * @param e
     * @return
     */
    @ExceptionHandler(value = BusinessException.class)
    @ResponseBody
    public CommonResp validExceptionHandler(BusinessException e) {
        CommonResp commonResp = new CommonResp();
        LOG.warn("业务异常:{}", e.getCode().getDesc());
        commonResp.setSuccess(false);
        commonResp.setMessage(e.getCode().getDesc());
        return commonResp;
    }

    /**
     * 校验异常统一处理
     * @param e
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public CommonResp validExceptionHandler(Exception e) {
        CommonResp commonResp = new CommonResp();
        LOG.error("系统异常:", e);
        commonResp.setSuccess(false);
        commonResp.setMessage("系统出现异常,请联系管理员");
        return commonResp;
    }
}

@ControllerAdvice注解可以对Controller做一些统一的异常处理,或者数据的预处理

@ExceptionHandler(value = BindException.class)注解可以对BindException做统一的异常处理

我们只需要在controller中写这样一个类,不需要去用它,是SpringBoot框架自己去扫描@ControllerAdvice注解,由框架来使用

45、IntelliJ IDEA根据文件名查找文件

连续按两次shift

46、生成string方法

我们按住alt+insert键生成toString方法,toString方法主要是用来打印日志,选择super.toString

47、表实体是在domain文件中,BO、VO都可以复制domain中的实体

48、查询排序

service/CategoryService


public List<CategoryQueryResp> all() {
        CategoryExample categoryExample = new CategoryExample();
        // 正序排列
        categoryExample.setOrderByClause("sort asc");
        List<Category> categoryList = categoryMapper.selectByExample(categoryExample);

        // 列表复制
        List<CategoryQueryResp> list = CopyUtil.copyList(categoryList, CategoryQueryResp.class);

        return list;
    }

保存和更新小字段和大字段,如果更新条数为0则插入数据

service/DocService


            // 更新
            docMapper.updateByPrimaryKey(doc);
            int count = contentMapper.updateByPrimaryKeyWithBLOBs(content);
            if (count == 0) {
                contentMapper.insert(content);
            }

49、查找用户名是否重复

service/UserService


            User userDB = selectByLoginName(req.getLoginName());
            if (ObjectUtils.isEmpty(userDB)) {
                // 新增
                user.setId(snowFlake.nextId());
                userMapper.insert(user);
            } else {
                // 用户名已存在
                throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);
            }

50、自定义异常

exception/BusinessException


public class BusinessException extends RuntimeException{

    private BusinessExceptionCode code;

    public BusinessException (BusinessExceptionCode code) {
        super(code.getDesc());
        this.code = code;
    }

    public BusinessExceptionCode getCode() {
        return code;
    }

    public void setCode(BusinessExceptionCode code) {
        this.code = code;
    }

    /**
     * 不写入堆栈信息,提高性能
     */
    @Override
    public Throwable fillInStackTrace() {
        return this;
    }
}

exception/BusinessExceptionCode


public enum BusinessExceptionCode {

    USER_LOGIN_NAME_EXIST("登录名已存在"),
    LOGIN_USER_ERROR("用户名不存在或密码错误"),
    VOTE_REPEAT("您已点赞过"),
    ;

    private String desc;

    BusinessExceptionCode(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

使用

service/UserService


    // 用户名已存在
    throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);

因为在controller/ControllerExceptionHandler文件中由@ControllerAdvice,SpringBoot框架自己去扫描@ControllerAdvice注解,由框架来使用,最终Spring会统一处理

51、跟新用户信息:

service/UserService


            user.setLoginName(null);
            user.setPassword(null);
            userMapper.updateByPrimaryKeySelective(user);

每次更新用户信息用户名和密码不需要修改,所以用户名和密码设置为null

updateByPrimaryKeySelective表示如果user里面的属性有值,我才去更新,没有值我就不更新这个字段

52、密码加密处理

service/UserService


req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));

53、集成redis

pom.xml

        <!--整合redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

redis配置地址

application.properties

# redis配置
spring.redis.host=r-uf6ljbcdaxobsifyctpd.redis.rds.aliyuncs.com
spring.redis.port=6379
spring.redis.password=Redis000

使用

controller/UserController


    @Resource
    private RedisTemplate redisTemplate;
    // redis信息存入
    Long tokenKey = snowFlake.nextId();
    redisTemplate.opsForValue().set(tokenKey.toString(), JSONObject.toJSONString(userLoginResp), 3600 * 24, TimeUnit.SECONDS);
    // redis信息删除
    redisTemplate.delete(token);

54、接口增加登录校验

interceptor/LoginInterceptor


/**
 * 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class);

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 打印请求信息
        LOG.info("------------- LoginInterceptor 开始 -------------");
        long startTime = System.currentTimeMillis();
        request.setAttribute("requestStartTime", startTime);

        // OPTIONS请求不做校验,
        // 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验
        if(request.getMethod().toUpperCase().equals("OPTIONS")){
            return true;
        }

        String path = request.getRequestURL().toString();
        LOG.info("接口登录拦截:,path:{}", path);

        //获取header的token参数
        String token = request.getHeader("token");
        LOG.info("登录校验开始,token:{}", token);
        if (token == null || token.isEmpty()) {
            LOG.info( "token为空,请求被拦截" );
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
        Object object = redisTemplate.opsForValue().get(token);
        if (object == null) {
            LOG.warn( "token无效,请求被拦截" );
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        } else {
            LOG.info("已登录:{}", object);
            LoginUserContext.setUser(JSON.parseObject((String) object, UserLoginResp.class));
            return true;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        long startTime = (Long) request.getAttribute("requestStartTime");
        LOG.info("------------- LoginInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        LOG.info("LogInterceptor 结束");
    }
}

使用

config/SpringMvcConfig


@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

    @Resource
    LoginInterceptor loginInterceptor;

    @Resource
    ActionInterceptor actionInterceptor;

    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/test/**",
                        "/redis/**",
                        "/user/login",
                        "/category/all",
                        "/ebook/list",
                        "/doc/all/**",
                        "/doc/vote/**",
                        "/doc/find-content/**",
                        "/ebook-snapshot/**",
                        "/ebook/upload/avatar",
                        "/file/**"
                );

        registry.addInterceptor(actionInterceptor)
                .addPathPatterns(
                        "/*/save",
                        "/*/delete/**",
                        "/*/reset-password");
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/file/**").addResourceLocations("file:D:/file/wiki/");
    }
}

55、线程本地变量使用【类似前端localStorage】

util/RequestContext


public class RequestContext implements Serializable {

    private static ThreadLocal<String> remoteAddr = new ThreadLocal<>();

    public static String getRemoteAddr() {
        return remoteAddr.get();
    }

    public static void setRemoteAddr(String remoteAddr) {
        RequestContext.remoteAddr.set(remoteAddr);
    }

}

线程本地变量remoteAddr、可以创建多个线程本地变量,线程之间不会互相干扰

使用

service/DocService


// 远程IP+doc.id作为key,24小时内不能重复
String ip = RequestContext.getRemoteAddr();

56、redis存取封装

util/RedisUtil


@Component
public class RedisUtil {

    private static final Logger LOG = LoggerFactory.getLogger(RedisUtil.class);

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * true:不存在,放一个KEY
     * false:已存在
     * @param key
     * @param second
     * @return
     */
    public boolean validateRepeat(String key, long second) {
        if (redisTemplate.hasKey(key)) {
            LOG.info("key已存在:{}", key);
            return false;
        } else {
            LOG.info("key不存在,放入:{},过期 {} 秒", key, second);
            redisTemplate.opsForValue().set(key, key, second, TimeUnit.SECONDS);
            return true;
        }
    }
}

validateRepeat检验redies,没有值添加,有值更新

使用

service/DocService


redisUtil.validateRepeat("DOC_VOTE_" + id + "_" + ip, 3600 * 24)

57、SpringBoot定时任务示例

启用定时任务

config/WikiApplication


@EnableAsync
public class WikiApplication {
}

job/TestJob


@Component
 public class TestJob {

    private static final Logger LOG = LoggerFactory.getLogger(TestJob.class);

    /**
     * 固定时间间隔,fixedRate单位毫秒
     */
    @Scheduled(fixedRate = 1000)
    public void simple() throws InterruptedException {
        SimpleDateFormat formatter = new SimpleDateFormat("mm:ss");
        String dateString = formatter.format(new Date());
        Thread.sleep(2000);
        LOG.info("每隔5秒钟执行一次: {}", dateString);
    }

    /**
     * 自定义cron表达式跑批
     * 只有等上一次执行完成,下一次才会在下一个时间点执行,错过就错过
     */
    @Scheduled(cron = "*/1 * * * * ?")
    public void cron() throws InterruptedException {
        SimpleDateFormat formatter = new SimpleDateFormat("mm:ss SSS");
        String dateString = formatter.format(new Date());
        Thread.sleep(1500);
        LOG.info("每隔1秒钟执行一次: {}", dateString);
    }

 }

所有定时器都是同一个线程

只有等上一次执行完成,下一次才会在下一个时间点执行,错过就错过

58、日志流水号的使用

日志位置

logback-spring.xml


<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>

LOG_ID就是日志流水号

在线程入口处设置日志流水号LOG_ID

aspect/LogAspect.java


    @Before("controllerPointcut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {

        // 增加日志流水号
        MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
    }

59、webSocket使用示例

依赖引入

pom.xml

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

配置

config/WebSocketConfig


@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

开启websocket服务

websocket/WebSocketServer【类似controller接收客户端请求】


@Component
@ServerEndpoint("/ws/{token}")
public class WebSocketServer {
    private static final Logger LOG = LoggerFactory.getLogger(WebSocketServer.class);

    /**
     * 每个客户端一个token
     */
    private String token = "";

    private static HashMap<String, Session> map = new HashMap<>();

    /**
     * 连接成功
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) {
        map.put(token, session);
        this.token = token;
        LOG.info("有新连接:token:{},session id:{},当前连接数:{}", token, session.getId(), map.size());
    }

    /**
     * 连接关闭
     */
    @OnClose
    public void onClose(Session session) {
        map.remove(this.token);
        LOG.info("连接关闭,token:{},session id:{}!当前连接数:{}", this.token, session.getId(), map.size());
    }

    /**
     * 收到消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        LOG.info("收到消息:{},内容:{}", token, message);
    }

    /**
     * 连接错误
     */
    @OnError
    public void onError(Session session, Throwable error) {
        LOG.error("发生错误", error);
    }

    /**
     * 群发消息
     */
    public void sendInfo(String message) {
        for (String token : map.keySet()) {
            Session session = map.get(token);
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                LOG.error("推送消息失败:{},内容:{}", token, message);
            }
            LOG.info("推送消息:{},内容:{}", token, message);
        }
    }

}

推送服务

service/DocService


        // 推送消息
        Doc docDb = docMapper.selectByPrimaryKey(id);
        String logId = MDC.get("LOG_ID");
        wsService.sendInfo("【" + docDb.getName() + "】被点赞!", logId);

60、使用异步化解耦点赞通知功能

config/WikiApplication开启异步化支持,另起一个线程执行后续内容


@EnableAsync
public class WikiApplication {
}

使用

service/DocService


    /**
     * 点赞
     */
    public void vote(Long id) {
        // 推送消息
        Doc docDb = docMapper.selectByPrimaryKey(id);
        String logId = MDC.get("LOG_ID");
        wsService.sendInfo("【" + docDb.getName() + "】被点赞!", logId);
        // rocketMQTemplate.convertAndSend("VOTE_TOPIC", "【" + docDb.getName() + "】被点赞!");
    }

service/WsService


    @Async
    public void sendInfo(String message, String logId) {
        MDC.put("LOG_ID", logId);
        webSocketServer.sendInfo(message);
    }

注意@Async必须写到方法上【最近原则,类似js中async】

单一职责,这个方法只负责发送数据

service类上必须加上@service

61、事务处理

应用场景:

同时对两张表有增删改的操作,就要考虑加事务,否则会造成数据不准确。当然也有不加事务的场景,不能一概而论

使用事务,直接添加@Transactional,spring就会自动处理事务

【先处理sql,再处理redis】


    @Transactional
    public void save(DocSaveReq req) {
            ...
            docMapper.insert(doc);
            contentMapper.insert(content);
    }

注意:同一个类里面A去调用B,B加事务注解不生效

62、使用rocketmq解耦【替代多线程逻辑】

(1)需要启动一个ma服务器

(2)我们SpringBoot就是MQ的客户端

(3)客户端(发送方)可以王topic主题里面发送一个消息,服务端(接收方/消费方)可以监听这个主题

下载依赖

pom.xml

        <!-- RocketMQ-->
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.0.2</version>
        </dependency>

rocketmq配置地址

application.properties


# rocketmq配置
rocketmq.name-server=127.0.0.1:9876
rocketmq.producer.group=default

组默认写default,需要后续监听

【发送MQ】

service/DocService


rocketMQTemplate.convertAndSend("VOTE_TOPIC", "【" + docDb.getName() + "】被点赞!");

逻辑处理

【消费MQ】

rocketmq/VoteTopicConsumer


 @Service
 @RocketMQMessageListener(consumerGroup = "default", topic = "VOTE_TOPIC")
 public class VoteTopicConsumer implements RocketMQListener<MessageExt> {

     private static final Logger LOG = LoggerFactory.getLogger(VoteTopicConsumer.class);

     @Resource
     public WebSocketServer webSocketServer;

     @Override
     public void onMessage(MessageExt messageExt) {
         byte[] body = messageExt.getBody();
         LOG.info("ROCKETMQ收到消息:{}", new String(body));
         webSocketServer.sendInfo(new String(body));
     }
 }

注意:@RocketMQMessageListener(consumerGroup = "default", topic = "VOTE_TOPIC")

consumerGroup与配置中consumerGroup对应,topic与mq发送的topic对应

62、复杂报表统计方案:

中间表统计:定时将业务表数据汇总到中间表,报表数据从中间表获取

63、打包分配二级域名位置

application.yml

server:
  port: 8080
  servlet:
    context-path: /dist

jean上健康告警配置

:8080/dist/monitor.html

修改后重新刷新maven

63、@Override用法

加了此注解的方法,表示此方法是一个覆写的方法,如果不满足覆写会报错。

在日常的编程中,一般出现在ServiceImpl实现类的方法上。java在进行接口实现的时候,要实现接口中定义的所有方法,也就是通过实现类重写方法。加上此注解,会帮助我们检测方法重写的正确性,例如:方法名是否与接口中一致(也就是是否可以在接口中找到次方法定义),方法的引用参数是否正确等等。。。

64、业务错误捕获

service


    try {
        throw new BizException(ResponseEnum.NO_SUCCESS);
    }
    catch (BizException bx) {
        throw bx;
    }
    catch (Exception ex) {
        throw ex;
    }

controller


        try {
            // service方法调用
        }catch (BizException bx){
            LogUtils.warn(String.format("业务异常【%s】【%s】");
            response.setCode(bx.getCode());
            response.setMessage(bx.getMessage());
        }catch (Exception ex){
            LogUtils.error(String.format("系统异常【%s】【%s】");
            response.setCode(ResponseEnum.SYSTEM_ERROR.code);
            response.setMessage(ResponseEnum.SYSTEM_ERROR.message);
        }

65、compareTo比较

payInfo.getPayAmount().compareTo(
    BigDecimal.valueOf(payResponse.getOrderAmount())) 
        == 0

66、直接取配置中的内容

@ConfigurationProperties(prefix = "alipay")

67、md5加密

        //MD5摘要算法(Spring自带) + 指定编码格式
		user.setPassword(DigestUtils.md5DigestAsHex(
				user.getPassword().getBytes(StandardCharsets.UTF_8)
		));

68、判断数据库是否写入成功

        //写入数据库
		int resultCount = userMapper.insertSelective(user);
		if (resultCount == 0) {
			return ResponseVo.error(ERROR);
		}

69、单元测试完成回滚,不对数据库进行污染

@Transactional
public class UserServiceImplTest extends MallApplicationTests {
}

70、公共返回json

@Data
public class ResponseVo<T> {

	private Integer status;

	private String msg;

	private T data;

	private ResponseVo(Integer status, String msg) {
		this.status = status;
		this.msg = msg;
	}

	private ResponseVo(Integer status, T data) {
		this.status = status;
		this.data = data;
	}

	public static <T> ResponseVo<T> successByMsg(String msg) {
		return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(), msg);
	}

	public static <T> ResponseVo<T> success(T data) {
		return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(), data);
	}

	public static <T> ResponseVo<T> success() {
		return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getDesc());
	}

	public static <T> ResponseVo<T> error(ResponseEnum responseEnum) {
		return new ResponseVo<>(responseEnum.getCode(), responseEnum.getDesc());
	}

	public static <T> ResponseVo<T> error(ResponseEnum responseEnum, String msg) {
		return new ResponseVo<>(responseEnum.getCode(), msg);
	}

	public static <T> ResponseVo<T> error(ResponseEnum responseEnum, BindingResult bindingResult) {
		return new ResponseVo<>(responseEnum.getCode(),
				Objects.requireNonNull(bindingResult.getFieldError()).getField() + " " + bindingResult.getFieldError().getDefaultMessage());
	}
}

71、公共返回错误码

@Getter
public enum ResponseEnum {

	ERROR(-1, "服务端错误"),

	SUCCESS(0, "成功"),

	PASSWORD_ERROR(1,"密码错误"),

	USERNAME_EXIST(2, "用户名已存在"),

	PARAM_ERROR(3, "参数错误"),

	EMAIL_EXIST(4, "邮箱已存在"),

	NEED_LOGIN(10, "用户未登录, 请先登录"),

	USERNAME_OR_PASSWORD_ERROR(11, "用户名或密码错误"),

	PRODUCT_OFF_SALE_OR_DELETE(12, "商品下架或删除"),

	PRODUCT_NOT_EXIST(13, "商品不存在"),

	PROODUCT_STOCK_ERROR(14, "库存不正确"),

	CART_PRODUCT_NOT_EXIST(15, "购物车里无此商品"),

	DELETE_SHIPPING_FAIL(16, "删除收货地址失败"),

	SHIPPING_NOT_EXIST(17, "收货地址不存在"),

	CART_SELECTED_IS_EMPTY(18, "请选择商品后下单"),

	ORDER_NOT_EXIST(19, "订单不存在"),

	ORDER_STATUS_ERROR(20, "订单状态有误"),

	;

	Integer code;

	String desc;

	ResponseEnum(Integer code, String desc) {
		this.code = code;
		this.desc = desc;
	}
}

72、表单验证

form/UserLoginForm.java

@Data
public class UserLoginForm {

	@NotBlank
	private String username;

	@NotBlank
	private String password;
}

73、公共异常捕获 

exception/RuntimeExceptionHandler.java

@ControllerAdvice
public class RuntimeExceptionHandler {

	@ExceptionHandler(RuntimeException.class)
	@ResponseBody
//	@ResponseStatus(HttpStatus.FORBIDDEN)
	public ResponseVo handle(RuntimeException e) {
		return ResponseVo.error(ERROR, e.getMessage());
	}

	@ExceptionHandler(UserLoginException.class)
	@ResponseBody
	public ResponseVo userLoginHandle() {
		return ResponseVo.error(ResponseEnum.NEED_LOGIN);
	}

	@ExceptionHandler(MethodArgumentNotValidException.class)
	@ResponseBody
	public ResponseVo notValidExceptionHandle(MethodArgumentNotValidException e) {
		BindingResult bindingResult = e.getBindingResult();
		Objects.requireNonNull(bindingResult.getFieldError());
		return ResponseVo.error(ResponseEnum.PARAM_ERROR,
				bindingResult.getFieldError().getField() + " " + bindingResult.getFieldError().getDefaultMessage());
	}
}

首先,ControllerAdvice本质上是一个Component,因此也会被当成组建扫描,一视同仁,扫扫扫。

@ControllerAdvice 配合 @ExceptionHandler 实现全局异常处理

74、 忽略大小写

user.getPassword().equalsIgnoreCase

75、session设置过期时间

server:
  servlet:
    session:
      timeout: 120

76、设置拦截器

UserLoginInterceptor.java

@Slf4j
public class UserLoginInterceptor implements HandlerInterceptor {

	/**
	 * true 表示继续流程,false表示中断
	 * @param request
	 * @param response
	 * @param handler
	 * @return
	 * @throws Exception
	 */
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		log.info("preHandle...");
		User user = (User) request.getSession().getAttribute(MallConst.CURRENT_USER);
		if (user == null) {
			log.info("user=null");
			throw new UserLoginException();

//			response.getWriter().print("error");
//			return false;
//			return ResponseVo.error(ResponseEnum.NEED_LOGIN);
		}
		return true;
	}
}

InterceptorConfig.java

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new UserLoginInterceptor())
				.addPathPatterns("/**")
				.excludePathPatterns("/error", "/user/login", "/user/register", "/categories", "/products", "/products/*");
	}
}

77、@PathVariable注解使用

    @GetMapping("/products/{productId}")
	public ResponseVo<ProductDetailVo> detail(@PathVariable Integer productId) {
		return productService.detail(productId);
	}

78、金额处理

    BigDecimal cartTotalPrice = BigDecimal.ZERO;
	cartTotalPrice = cartTotalPrice.add(cartProductVo.getProductTotalPrice());

79、测试用例(执行先后顺序)

@Before

@Test

@After

80、sql执行判断成功通过row

    @Override
	public ResponseVo delete(Integer uid, Integer shippingId) {
		int row = shippingMapper.deleteByIdAndUid(uid, shippingId);
		if (row == null || theCount <= 0) {
			return ResponseVo.error(ResponseEnum.DELETE_SHIPPING_FAIL);
		}

		return ResponseVo.success();
	}

81、数据处理方法可以的单独抽离

    private OrderVo buildOrderVo(Order order, List<OrderItem> orderItemList, Shipping shipping)     {
		OrderVo orderVo = new OrderVo();
		BeanUtils.copyProperties(order, orderVo);

		List<OrderItemVo> OrderItemVoList = orderItemList.stream().map(e -> {
			OrderItemVo orderItemVo = new OrderItemVo();
			BeanUtils.copyProperties(e, orderItemVo);
			return orderItemVo;
		}).collect(Collectors.toList());
		orderVo.setOrderItemVoList(OrderItemVoList);

		if (shipping != null) {
			orderVo.setShippingId(shipping.getId());
			orderVo.setShippingVo(shipping);
		}

		return orderVo;
	}

82、list转map

        Map<Long, List<OrderItem>> orderItemMap = orderItemList.stream()
				.collect(Collectors.groupingBy(OrderItem::getOrderNo));

 以直接引用已有Java类或对象的方法或构造器。方法引用

 83、启动类扫包路径

@SpringBootApplication
@EnableJpaAuditing
@ComponentScan(basePackages = {"com.geekbang"})
@EnableTransactionManagement
//用于扫描Dao @Repository
@EnableJpaRepositories(basePackages = {"com.geekbang"})
//用于扫描JPA实体类 @Entity,默认扫本包当下路径
@EntityScan(basePackages = {"com.geekbang"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

 @SpringBootApplication
@EnableJpaAuditing
@ComponentScan(basePackages = {"com.geekbang"})
@EnableTransactionManagement
//用于扫描Dao @Repository
@EnableJpaRepositories(basePackages = {"com.geekbang"})
//用于扫描JPA实体类 @Entity,默认扫本包当下路径
@EntityScan(basePackages = {"com.geekbang"}

84、返回空数组

Collections.emptyList()

85、跨域设置

controller

@CrossOrigin(origins = "*")

86、识别子类型

@Autowired
@Qualifier("taskExecutor")
private ThreadPoolTaskExecutor threadPoolTaskExecutor;

87、接口参数获取为null

检查每个参数的类型是否争取

88、复制时间处理

result.settime(DateUtil.date2String(time, DateUtil.DATE_PATTERN_YYYY_MM_DD_HH_MM_SS));

89、properties-转yaml

在线yaml转properties-在线properties转yaml-ToYaml.com

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
springboot学习资料 一、 Spring介绍 1 1.1、SpringBoot简介 1 1.2、系统要求: 1 二、快速入门 1 2.1、创建一个Maven工程 1 2.2、pom文件引入依赖 2 2.3、编写HelloWorld服务 2 2.4、@RestController 3 2.5、@EnableAutoConfiguration 3 2.6 SpringApplication.run(HelloController.class, args); 3 @SpringbootApplication 3 2.7、SpringBoot启动方式1 4 2.8、SpringBoot启动方式2 4 三、 Web开发 5 3.1、静态资源访问 5 3.2、全局捕获异常 5 3.3、渲染Web页面 6 3.4、使用Freemarker模板引擎渲染web视图 6 3.4.1、pom文件引入: 6 3.4.2、后台代码 6 3.4.3、前台代码 7 3.4.4、Freemarker其他用法 7 3.4.5、Freemarker配置 8 3.5、使用JSP渲染Web视图 8 3.5.1、pom文件引入以下依赖 8 3.5.2、在application.properties创建以下配置 9 3.5.3、后台代码 9 四、 数据访问 9 4.1、springboot整合使用JdbcTemplate 9 4.2、springboot整合使用mybatis 9 4.3、springboot整合使用springjpa 9 4.4、springboot整合多数据源 9 五、 事物管理 9 5.1.1springboot整合事物管理 9 5.1.2SpringBoot分布式事物管理 9 六、 日志管理 9 6.1使用log4j记录日志 9 6.2使用AOP统一处理Web请求日志 9 七、 缓存支持 9 7.1注解配置与EhCache使用 9 使用Redis做集中式缓存 9 八、 其他内容 9 8.1、使用@Scheduled创建定时任务 9 8.2、使用@Async实现异步调用 9 8.3、自定义参数 9 8.4、多环境配置 9 8.5、修改端口号 9 8.6、SpringBoot yml 使用 9 8.7、发布打包 9
文档内容 一、 Spring介绍 1 1.1、SpringBoot简介 1 1.2、系统要求: 1 1.3、SpringBootSpringMVC区别 1 1.4、SpringBootSpringCloud区别 2 1.5常见错误 2 二、快速入门 2 2.1、创建一个Maven工程 2 2.2、pom文件引入依赖 3 2.3、编写HelloWorld服务 3 2.4、@RestController 4 2.5、@EnableAutoConfiguration 4 2.6 SpringApplication.run(HelloController.class, args); 4 2.7、SpringBoot启动方式1 4 2.8、SpringBoot启动方式2 4 2.9、SpringBoot启动方式3 5 三、 Web开发 5 3.1、静态资源访问 5 3.2、渲染Web页面 5 3.3、使用Freemarker模板引擎渲染web视图 6 3.3.1、pom文件引入: 6 3.3.2、后台代码 6 3.3.3、前台代码 6 3.3.4、Freemarker其他用法 7 3.3.5、Freemarker配置 8 3.4、使用JSP渲染Web视图 8 3.4.1、pom文件引入以下依赖 8 3.4.2、在application.properties创建以下配置 9 3.4.3、后台代码 9 3.5、全局捕获异常 10 四、 数据访问 10 4.1、springboot整合使用JdbcTemplate 10 4.2、springboot整合使用mybatis 12 4.3、springboot整合使用springjpa 18 4.4、springboot整合多数据源 19 五、 事物管理 25 5.1.1SpringBoot整合事物管理 25 5.1.2SpringBoot分布式事物管理 25 六、 日志管理 30 6.1使用log4j记录日志 30 6.2使用AOP统一处理Web请求日志 32 6.3Spring Boot集成lombok让代码更简洁 33 七、 缓存支持 35 7.1注解配置与EhCache使用 35 7.2使用Redis集成缓存 37 八、 热部署 37 8.1 什么是热部署 37 8.2 项目演示案例 37 8.3 热部署原理 37 8.4 Devtools依赖 38 8.5 Devtools原理 38 九、 监控管理 38 Actuator监控应用 38 Maven依赖 38 YML配置 39 Actuator访问路径 40 Admin-UI分布式微服务监控中心 40 Admin-UI-Server 40 Admin-UI-Client 41 十、 性能优化 43 组件自动扫描带来的问题 43 将Servlet容器变成Undertow 44 SpringBoot JVM参数调优 44 十一、 2.0版本新特性 45 以Java 8 为基准 45 内嵌容器包结构调整 45 Servlet-specific 的server properties调整 45 Actuator 默认映射 46 Spring Loaded不再支持 46 支持Quartz Scheduler 46 OAuth 2.0 支持 46 支持Spring WebFlux 46 版本要求 46 十二、 其他内容 47 12.1、使用@Scheduled创建定时任务 47 12.2、使用@Async实现异步调用 47 12.3、自定义参数 49 12.4、多环境配置 50 12.5、修改端口号 50 12.6、SpringBoot yml 使用 50 12.7、SpringBoot整合拦截器 51 12.8、发布打包 52
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值