【项目经验】Spring Boot 搭建记录+错误经验(已完结)

本文详细记录了使用Spring Boot和MyBatis搭建项目的全过程,包括配置DataSource、SessionFactory、事务管理、Controller、Service、DAO等,并分享了遇到的错误和解决方法。此外,还介绍了Spring Boot的文挡操作、事务管理、表单验证、AOP日志处理和统一异常处理等进阶内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spring问题汇总 (on IDEA MAC)
https://blog.youkuaiyun.com/weixin_42915286/article/details/85017091
————————————————

SpringBoot+MyBatis搭建WeChat程序

SPRING INITIALIZR:
http://start.spring.io

IDEA on MAC
可根据右侧目录查询章节;

项目源自:https://www.imooc.com/learn/945
图片为Yaau原创,转载需标明来源:https://blog.youkuaiyun.com/weixin_42915286

1.POM(把Mybatis注释掉,暂不需要)
2.DemoApplication
3.hello 可以启动了
4.Entity类完成AREA 实体类,把表头输进项目
5.实体类完成后,自底向上开发DAO层:增删改查
  1.配置:POM(MAVEN)+ Mybatis + DAO
    (1)POM中把第一步对Mybatis的注释去掉,依赖中添加MySQL驱动和连接池;
    (2)resources下新建mybatis-config.xml,配置mybatis;
    (3)在demo下创建文件夹:
        config - dao:DatasourceConfiguration,连接数据库,为其服务
       (SSM喜欢在xml内配置,SB喜欢在代码内配置)
       (这里的数据直接用的本地数据库)!!!!!
        & 同时亦要配置SessionFactoryConfiguration,其中配置Mapper
        (Mapper:项目编写的数据(对数据库的请求)调用JDBC,转换成数据库能识别的语言,即SQL语句,来操作数据库。返回结果,能将结果映射成数据,赋值到实体语句中)
  2.接口:新建demo-dao:AreaDao接口,根据已定义的Area实体类,定义增删改查功能
  3.针对接口编制Mapper的实现:AreaDao.xml 增删改查的配置 DAO编写完成
  4.对AreaDao接口做UT(单元测试),验证增删改查
6.Service层:整合复杂的业务逻辑;要么验证成功,要么抛出异常
  根据注入的AreaDao(直接引入本地数据库的类)来判断
  尽可能将所有业务逻辑都放到Service层中
  因内含业务逻辑,运行前需要先进行空值的判断
 (如每个DAO的表都有增删改查功能,Service帮他们的增删表查功能整合到一起)
      (AreaDao中点“AreaDao”,option+return - Create Test,选中实现UT的方法们)
      (又如,创建店铺目录图片分三步:1.数据库存资料 2.创建目录  
        3.往目录里放图片,若第二步失败了,全局崩溃,第一步也不复存在;Service帮助控制全局)
        (就如同try catch语句,如果没有try的话,出现异常会导致程序崩溃。
而try则可以保证程序的正常运行下去)
  1.配置:创建config - service 新建事务管理类 建立个事务管理Manager的Bean,注入数据库。
  2.接口:AreaService。Service中的方法和DAO的方法不一定一致,此处一致纯属巧合
  3.实现类:实际上就是把每个方法用try catch语句块过一遍,成功返true,失败报错

7.业务Controller方法的实现(Controller层:demo下的web文件夹)
  1.业务Controller方法的实现:用@RestController。包括增删改查。
  2.统一异常处理类,对项目Controller DAO Service可能抛出的异常做统一处理。
    让我们专注于业务的逻辑
    handler是所有处理类的集合,下面新建所有异常处理类Global

概览图

在这里插入图片描述

DataSourceConfiguration

在这里插入图片描述


import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.beans.PropertyVetoException;

@Configuration
@MapperScan({"com.example.mall_boot.config.dao"})
public class DataSourceConfiguration {

    @Value("${jdbc.driver}")
    private String jdbcDriver;
    @Value("${jdbc.url}")
    private String jdbcUrl;
    @Value("${jdbc.username}")
    private String jdbcUsername;
    @Value("${jdbc.password}")
    private String jdbcPassword;

    public DataSourceConfiguration(){}

    @Bean(
            name = {"dataSource"}
    )
    public ComboPooledDataSource createDateSource() throws PropertyVetoException {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setDriverClass(this.jdbcDriver);
        dataSource.setJdbcUrl(this.jdbcUrl);
        dataSource.setUser(this.jdbcUsername);
        dataSource.setPassword(this.jdbcPassword);
        dataSource.setAutoCommitOnClose(false);
        return dataSource;
    }
}

SessionFactoryConfiguration标题

注意!若private DataSource dataSource还报错,可能是引用类型有误,应该是
import javax.sql.DataSource;
或demoController应该放在正确位置;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.io.IOException;

@Configuration
public class SessionFactoryConfiguration {
    @Value("${mybatis_config_file}")
    private String mybatisConfigFilePath;
    @Value("${mapper_path}")
    private String mapperPath;
    @Value("${entity_package}")
    private String entityPackage;

    @Autowired
    @Qualifier("dataSource")
    private DataSource dataSource;

    public SessionFactoryConfiguration(){}

    @Bean(name = {"sqlSessionFactory"})

    public SqlSessionFactoryBean createSqlSessionFactoryBean() throws IOException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setConfigLocation(new ClassPathResource(this.mybatisConfigFilePath));
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        String packageSearchPath = "classpath*:" + this.mapperPath;
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources(packageSearchPath));
        sqlSessionFactoryBean.setDataSource(this.dataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage(this.entityPackage);
        return sqlSessionFactoryBean;
    }
}

TransactionManagementConfiguration在这里插入图片描述

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class TransactionManagementConfiguration implements TransactionManagementConfigurer {
    @Autowired
    private DataSource dataSource;

    @Override
    public PlatformTransactionManager annotationDrivenTransactionManager(){
        return new DataSourceTransactionManager(this.dataSource);
    }
}

AreaDao.class在这里插入图片描述

Area在这里插入图片描述

GlobalExceptionHandler

在这里插入图片描述

AreaServiceImpl

在这里插入图片描述

AreaService

在这里插入图片描述

AreaController

在这里插入图片描述
若报错,试试:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@MapperScan("com.xxx.dao")

DemoApplication

必须有@SpringBootApplication
在这里插入图片描述

hello

在这里插入图片描述

AreaDao.xml

在这里插入图片描述

application.properties

默认文件是.properties,但是建议直接新建.yml文件
在这里插入图片描述
.yml文件要注意:冒号后面必须有一个空格

server:
  port: //
  servlet.context-path: // /springboot

jdbc:
    driver: //com.mysql.cj.jdbc.Driver
    password: //
    url: //
    username: //
mapper_path: //
mybatis_config_file: //
entity_package: //

Mybatis-config.xml

在这里插入图片描述

AreaDao Test

在这里插入图片描述

DemoApplicationTests

在这里插入图片描述

DataSourceConfiguration

在这里插入图片描述

POM.xml

spring-boot-starter 必须要有
在这里插入图片描述

External Libraries

在这里插入图片描述

————————————————————————————————————————

IDEA新建一个SB工程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
POM模版:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.verison>1.8</java.verison>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

</project>

写一个类:

@RestController
public class Controller {

    @RequestMapping(value="/hello",method = RequestMethod.GET)
    public String say(){
        return "Hello!";
    }
}

即可访问。

若报错:Failed to configure a DataSource: 'url' attribute is not specified and no embedde
Controller中@SpringBootApplication标签换成:@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

————————————————————————————————————————

文字详解

Spring问题汇总 (on IDEA MAC)
https://blog.youkuaiyun.com/weixin_42915286/article/details/85017091

Controller.java

@RestController
public class HelloController {

    @Value("${age}")
    private Integer age;

    @RequestMapping(value="/hello",method = RequestMethod.GET)
    public String say(){
        return age;
    }
}

Application.yml

server:
  port: 8080
  servlet.context-path: /springboot
  
age: 18

浏览器渲染 http://localhost:8080/springboot/hello

age: 18

———————————————————
Controller.java

@RestController
public class HelloController {

    @Value("${size}")
    private String size;

    @Value("${age}")
    private Integer age;

    @Value("${content}")
    private String content;

    @RequestMapping(value="/hello",method = RequestMethod.GET)
    public String say(){
        return content;
    }
}

Application.yml

server:
  port: 8080
  servlet.context-path: /springboot

size: A
age: 18
content: "size: ${size},age: ${age}"

浏览器渲染 http://localhost:8080/springboot/hello

size: A,age: 18

———————————————————
但是这样写有点累,一个属性要写一次;
下面新建一个类,在yml中指定前缀,一次搞定。
多环境配置推荐!!)(但此操作很基本,还有更优雅的操作)

HelloController

@RestController
public class HelloController {

    @Autowired
    private BodyProperties bodyproperties;

    @RequestMapping(value="/hello",method = RequestMethod.GET)
    public String say(){
        return bodyproperties.getSize();
    }
}

BodyProperties.java(新建)

@Component                               //一定要有,否则报错找不到Bean
@ConfigurationProperties(prefix="body")  //连接到yml中的“body”前缀
public class BodyProperties {

    private String size;
    private String age;

    public String getSize() {
        return size;
    }

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

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
}

Application.yml

server:
  port: 8080
  servlet.context-path: /springboot

body:
  size: B
  age: 18

浏览器渲染 http://localhost:8080/springboot/hello

B

——————————————————————————————————————
问题来了,如果Size想更改?
生产环境 production开发环境 develpment的差异,配置的更改问题;(-prod-dev
作为一名优秀的开发者,不应该把大量的时间都花在配置更改上。

Application-dev.yml

server:
  port: 8080
  servlet.context-path: /springboot

body:
  size: B
  age: 18

Application-prod.yml

server:
  port: 8081
  servlet.context-path: /springboot

body:
  size: F
  age: 18

Application.yml

spring:
  profiles:
    active: dev

浏览器渲染 http://localhost:8080/springboot/hello

B

———————————————————
Application-dev.yml

server:
  port: 8080
  servlet.context-path: /springboot

body:
  size: B
  age: 18

Application-prod.yml

server:
  port: 8081
  servlet.context-path: /springboot

body:
  size: F
  age: 18

Application.yml

spring:
  profiles:
    active: prod

浏览器渲染 http://localhost:8081/springboot/hello

F

——————————————————————————————————————

Controller的使用

  • @Controller 处理HTTP请求
  • @RestController Spring4之后新加的注解,等同于@ResponseBody配合@Controller(老方法)
  • @RequestMapping 配置url映射
    备注:@ResponseBody配合@Controller(老方法,需要生成模版spring-boot-starter-thymeleaf
    【JAVA实践前后端分离】【后端】就是指:提供REST接口,返回JSON格式给前端;
    不再使用模版(会给性能上带来强大损耗)。

———————————————————
若想使多个域名都指到同一页面?
比如http://localhost:8080/springboot/hello``http://localhost:8080/springboot/hi都可以访问到指定页面;
事关 @RequestMapping ,可以这么写:@RequestMapping(value={"/hello","/hi"},method = RequestMethod.GET)

HelloController

@RestController
public class HelloController {
。。。
    @RequestMapping(value={"/hello","/hi"},method = RequestMethod.GET)
    public String say(){
        return bodyproperties.getSize();
    }
}

———————————————————
若想给域名分类,便于直观管理?
比如想使域名成为:http://localhost:8080/hello/say

HelloController

@RestController
@RequestMapping("/hello")
public class HelloController {
。。。
    @RequestMapping(value={"/say"},method = RequestMethod.GET)
    public String say(){
        return bodyproperties.getSize();
    }
}

(当然,此项目中.ymlserver: servlet.context-path: /springboot要删除,URL中才可以不写/springboot
——————————————————————————————————————
Controller中更改@RequestMapping的方法,GET改成POST后,不能从浏览器检验成效,需要在第三方软件中检验;
如:可视化测试工具 POSTMAN;

HelloController

@RestController
@RequestMapping("/hello")
public class HelloController {

    @Autowired
    private BodyProperties bodyproperties;

    @RequestMapping(value="/say",method = RequestMethod.POST)
    public String say(){
        return bodyproperties.getSize();
    }
}

在这里插入图片描述
返回F,成功。
———————————————————
疑问:若不写method = RequestMethod.POST,POSTMAN中使用POST和PUT方法能否检验成功?

答案是可以的。
但是不推荐这样,因为GET和POST适用于不同场景,为求安全还是要加上为佳。

———————————————————

  • @PathVariable 获取URL中的数据
  • @RequestParam 获取请求参数的值
  • @GetMapping 组合注解

使用**@PathVariable**:

HelloController

@RestController
@RequestMapping("/hello")
public class HelloController {

    @Autowired
    private BodyProperties bodyproperties;

    @RequestMapping(value="/say/{id}",method = RequestMethod.GET)
    public String say(@PathVariable("id") Integer id){
        return "id:"+id;
    }
}

浏览器访问,URL尾端随意输入一个数字,如55http://localhost:8081/hello/say/55
浏览器渲染

id:55

value也可以写成value="/{id}/say"
同时URL也变了,如55http://localhost:8081/hello/55/say
———————————————————
这种URL看上去很简洁明了;
Anyway,传统的URL却不是这样的,比如http://localhost:8081/springboot/hello/say?id=55
这种格式的写法要用到:@RequestParam

HelloController

@RestController
@RequestMapping("/hello")
public class HelloController{
    @RequestMapping(value="/say",method = RequestMethod.GET)
    public String say(@RequestParam("id") Integer id){
        return "id:"+id;
    }
}

浏览器访问,URL尾端随意输入一个数字,如66http://localhost:8081/springboot/hello/say?id=66

浏览器渲染

id:66

注意!方法say内的RequestParam id和Integer id,不是同一个参数!
方法say内的RequestParam id浏览器中显示为id
Integer id浏览器中显示为66(随意输入的数字);

@RequestParam还有一个作用,即使在浏览器输入http://localhost:8081/springboot/hello/say?id=
即id值为空,也有返回页面:

id:null

@RequestParam还有一个作用,可以在括号中规定idrequireddefaultValue;(defaultValue的属性不是int,参数要加双引号)
如:

@RestController
@RequestMapping("/hello")
public class HelloController{
    @RequestMapping(value="/say",method = RequestMethod.GET)
    public String say(@RequestParam(value="id",required=false,defaultValue="0") Integer id){
        return "id:"+id;
    }
}

———————————————————
@GetMapping可以简化@RequestMapping的代码量:(推荐!)
原:@RequestMapping(value="/say",method = RequestMethod.GET)
现:@GetMapping(value="/say")

如:

@RestController
@RequestMapping("/hello")
public class HelloController{
    @GetMapping(value="/say")
    public String say(@RequestParam(value="id",required=false,defaultValue="0") Integer id){
        return "id:"+id;
    }
}

同样的,还可以写@PutMapping等等标签;
——————————————————————————————————————

数据库操作

了解Spring-Data-Jpa
JPA(Java Persistence API)定义了一系列对象持久化的标准(只是一个文本上的规范,不是组件/系统),目前实现这一规范的产品有Hibernate、TopLink等。

白话:就是Spring对Hibernate的整合;
———————————————————
依赖

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

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>

———————————————————

实例

请求类型 请求路径   功能
GET     /body    查询资料列表
POST    /body    创建一个资料
GET     /body/id 通过ID查询一个资料
PUT     /body/id 通过ID更新一个资料
DELETE  /body/id 通过ID删除一个资料

———————————————————
Application.yml

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/dbbody
    username: root
    password: 本人数据库为八位密码

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true //可以在控制台里看见SQL语句

需要先在MySQL中新建一个名为dbbody的数据库;
不报错,表示连接成功;
———————————————————

ddl-auto参数注解:

  • create:程序每一次跑的时候,都会创建一个新的表;若之前已创建一个表,程序运行时,会把旧表删掉;
    如:若运行了此方法后,在数据库中直接添加一个参数,再在IDE中再次运行程序,刚刚数据库里添加的参数回消失。
    Console:Hibernate: drop table if exists hibernate_sequence Hibernate: create table body (id integer not null, age integer, size varchar(255), primary key (id)) engine=MyISAM
  • update 最常用:第一次运行时也会新建表;
    create不同的是,若旧表中有数据,此方法不会再二次启动时删除原有数据。
  • create-drop:程序停下来的时候会把原有表格删掉;
  • none:什么都不做;
  • validate:验证IDE与数据库中的数据是否相同,不同则会报错;

———————————————————
现在数据库中还是空的,需不需要再数据库中新建表?
不需要,在IDE中新建一个类,即可把类中定义的属性与数据库中对应起来;
注: 要加上JPA的@Entity,方法内加上@Id @GeneratedValue,方法内必须加上一个无参构造器

Springboot文件夹下新建Body.java

@Entity
public class Body {

    @Id
    @GeneratedValue //ID自增
    private Integer id;
    private String size;
    private Integer age;

    public Body() {
    }

//生成几个属性的SETTER N GETTER方法

}

运行后,MySQL中查看dbbody数据库,刷新,生成了body表;

查看Table Info:

CREATE TABLE `body` (
  `id` int(11) NOT NULL,
  `age` int(11) DEFAULT NULL,
  `size` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

特别注意!!id一定要设置Auto_Increment,否则会报错!!
特别注意!!数据库会同时生成一个表hibernate_sequence,不能删除!!
因为它控制了下一个生成行的ID,设置在Body.java中的@GeneratedValue标签:存在@GeneratedValue(strategy = GenerationType.IDENTITY)等说法
此处吃过螺丝;
———————————————————

看回上面说到的实例

请求类型 请求路径   功能
GET     /body    查询资料列表
POST    /body    创建一个资料
GET     /body/id 通过ID查询一个资料
PUT     /body/id 通过ID更新一个资料
DELETE  /body/id 通过ID删除一个资料

此方法同步SQL非常简单,一句SQL语句也不需要;
———————————————————
此部分是GET请求:获取资料列表

BodyController

@RestController
public class BodyController {

    @Autowired
    private BodyRepository bodyRepository;

    //GET 查询
    @GetMapping(value= "/body")
    public List<Body> bodylist(){
        return bodyRepository.findAll();

    }
}

里面的BodyRepository是下面需要新建的一个接口文件;
———————————————————
BodyRepository (为interface)

public interface BodyRepository extends JpaRepository<Body,Integer> {
}

———————————————————
配置完毕;
这时在数据库中随便写两条数据;
用POSTMAN测试:
在这里插入图片描述
有返回结果,与数据库数据一致;
成功。
———————————————————
此部分是POST请求:新增资料

BodyController

    //POST 新增
    //这里属性的写法不是最佳方式

    @PostMapping(value="/body")
    public Body bodyAdd(@RequestParam("size") String size,@RequestParam("age") Integer age){
        Body body=new Body();
        body.setSize(size);
        body.setAge(age);

        return bodyRepository.save(body);
    }
    //属性过多时,这是优雅方式:把单个的属性换成对象
    @PostMapping(value="/body")
    public Body bodyAdd(Body body){
        body.setSize(body.getSize());
        body.setAge(body.getAge());

        return bodyRepository.save(body);
    }

POSTMAN验证:
在这里插入图片描述
查看数据库:
在这里插入图片描述
成功。
———————————————————
下面部分是GET PUT DELETE请求:通过ID查询/更新/删除一个信息(且路径一样)

此部分是GET请求:通过ID查询一个信息

BodyController

//通过ID查询
    @GetMapping(value="/body/{id}")
    public Body bodyFindOne(@PathVariable("id") Integer id){
        return bodyRepository.findById(id).get();
    }

注意:这里本来想采用return bodyRepository.findOne(id);
但只在springboot2.0以下的版本才能良好的支持findOne(String id)方法,而springboot2.0及其以上版本就只有findOne(Example<S> example)方法;
可以在POM中把Spring版本号改成1.5.10;不过那样工程量太大,我才用上述的方法也OK。

http://localhost:8081/springboot/body/1
在这里插入图片描述
———————————————————
此部分是PUT请求:通过ID更新一个信息

更新:肯定不止更新一个ID,还要更新其他参数,这里是sizeage

BodyController

    //PUT 通过ID更新

    @PutMapping(value="/body/{id}")
    public Body bodyUpdate(@PathVariable("id") Integer id,
                           @RequestParam("size") String size,
                           @RequestParam("age") Integer age){
        Body body=new Body();
        body.setId(id);
        body.setAge(age);
        body.setSize(size);

        return bodyRepository.save(body);
    }

在这里插入图片描述
本项目中:ID1
原:18 8
现:19 G

运行成功;
查看数据库,也更新成功。

———————————————————
此部分是DELETE请求:通过ID删除一个信息

BodyController

    //DELETE 通过ID删除

    @DeleteMapping(value="/body/{id}")
    public void bodyDelete(@PathVariable("id") Integer id){
        bodyRepository.deleteById(id);
    }

在这里插入图片描述
成功删除。

———————————————————

拓展:
除了通过ID查询资料,还可以通过age查询资料;

BodyRepository(interface)

添加一句

    //通过age来查询资料;因为age可能会重复,所以这里是个列表
    public List<Body> findByAge(Integer age);

BodyController

    //拓展:GET 通过age查询资料(列表)

    @GetMapping(value="/body/more/age/{age}")
    public List<Body> bodyListByAge(@PathVariable("age") Integer age){
        return bodyRepository.findByAge(age);
    }

在这里插入图片描述
——————————————————————————————————————

事务管理 Transactional

如果说希望同时插入2条数据,不然就都不能插入;不希望有1条成功,1条失败;
就涉及到事务管理了,是很重要和常见的部分;

只有希望同时操作多个数据时要用,只有查询时不需要

新建一个BodyService
当中要在类前@Service
方法内@Autowired Service
@Transactional保证了事务管理的关键(要么多条都上传成功,要么全失败);

@Service
public class BodyService {

    @Autowired
    private BodyRepository bodyRepository;

    @Transactional //此标签保证了必须同时插入2条数据
    public void insertTwo(){
        Body body1=new Body();
        body1.setSize("S");
        body1.setAge(23);
        bodyRepository.save(body1);

        Body body2=new Body();
        body2.setSize("T");
        body2.setAge(30);
        bodyRepository.save(body2);
    }
}

在Controller中添加@Autowired Service

    //事务管理 two

    @PostMapping(value="/body/two")
    public void body1(){
        bodyService.insertTwo();
    }

在POSTMAN输入URL后,点击Send,就等于把BodyService中的内容POST入数据库。

测试时间,我们可以这样安排:
先把数据库中数据清空,以便思路清晰;
sizelength改为1,然后把BodyService中的body2size改成TTT(这样就可以让body2不被通过;同时保证body1是可以通过的);
然后把@Transactional注释掉,看看没有@Transactional时,是否能只POST二条中的一条。POSTMAN运行URL;

成功,body2未上传,body1已上传;

接着,把@Transactional激活,数据库清空数据,再运行POSTMAN;

成功,一条数据都无。

(注:我在操作这一步骤时出现问题;配置了body2不通过而body1通过,且激活了@Transactional后,POSTMAN操作后,数据库仍上传了body1;后来把数据库中与body表并列的hibernate_sequence表清空后,才顺利达到目的。)

——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————
——————————————————————————————————————

进阶

目录:

1.表单验证 @Valid :
  比如规定要输入性别,但有人输入了姓名,防止这种行为;还有防黑客;

2.AOP处理请求日志:
  面向切面,避免些重复代码;log.info()输出日志比传统输出记录更完善;

3.统一异常处理:
  

4.单元测试 :
  每一个正规的开发行为最好都写单元测试,是有责任感的行为;

这里我们把之前配置的项目整理下:

BEFORE:
在这里插入图片描述
AFTER:
在这里插入图片描述
———————————————————

表单验证 @Valid

这里我们设定一个表单验证:拦截所有age小于18岁的资料;

还记得之前配置过POST增方法

    //属性过多时,这是优雅方式:把单个的属性换成对象
    @PostMapping(value="/body")
    public Body bodyAdd(Body body){
        body.setSize(body.getSize());
        body.setAge(body.getAge());

        return bodyRepository.save(body);
    }

如何拦截?
1. 在实体类Body.java中的private Integer age前添加标签 @Min ,写value限定数值,message返回语句;
2. 在POST方法的对象属性前添加标签 @Valid ,限定这个对象,后面属性加上 BindingResult 以及实体类;方法内添加if语句bindingResult.hasErrors(),输出语句,返回null值;

    //Body.java

    @Min(value=18,message = "未成年禁止报名")
    private Integer age;
    //BodyController.java

    @PostMapping(value="/body")
    public Body bodyAdd(@Valid Body body, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            System.out.println(bindingResult.getFieldError().getDefaultMessage());
            return null; //发生错误就不能往下走了
        }
        body.setSize(body.getSize());
        body.setAge(body.getAge());

        return bodyRepository.save(body);
    }

这时在POSTMAN添加一个age大于18的可成功;
添加一个小于18的会失败;
————————————————————————————

AOP处理请求日志

确定概念:
AOP是一种编程规范,非仅在JAVA中存在,是一种程序设计思想

程序设计思想也有AOP(面向切面)/ OOP(面向对象)/ POP(面向过程);

OOP和POP的区别?
比如下雨了:
POP:假如下雨了,我打开了雨伞;
OOP:实例化 天气—— 下雨;实例化 我—— 赋予打伞的动作(一种方法);
他们换了个角度看世界;

OOP将需求功能划分为垂直且相对独立的,会封装成不同的类,且有自己的行为;
AOP利用横切的技术,将庞大的类水平切割,并将影响到多个类的公共行为封装成可重复利用的模块——切面;即面向切面编程;

AOP思想:将通用逻辑从业务逻辑中分离出来;
在这里插入图片描述
在这里插入图片描述
——————————————————
如何在Spring中集成AOP并处理请求日志?

举例:配置需要判断登录后才能访问诸方法

若想设置这样的情况,传统方法是在bodyController的(比如)Get方法中插入if语句,如果登录了才可以进行下去;
现在只有一个Controller还好办,但如果太多控制器,每一个方法里都要定义一下,过于繁琐;
解决办法:使用AOP即可;

添加 POM

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

以往习惯可能是在BodyApplication启动类上加一个注解,但AOP不需这样;

新建文件夹aspect
新建一个类:HttpAspect

先试验看看 @Before ?(其中bodylist(..)的两点代表bodylist中所有方法)

//演示段落

@Aspect
@Component //把文件引入到Spring容器中
public class HttpAspect {
    @Before("execution(public * com.example.springboot.controller.BodyController.bodylist(..))")
    public void log(){
        System.out.println("111111");
    }
}

POSTMAN中 GET 127.0.0.1:8081/springboot/body
返回了所有资料信息;IDE也返回了111111;
但这里只拦截了bodylist;
拦截所有?比如包括POST方法?
bodylist(..)换成*(..)

再添加 @After 看看?

//演示段落

@Aspect
@Component //把文件引入到Spring容器中
public class HttpAspect {
    @Before("execution(public * com.example.springboot.controller.BodyController.*(..))")
    public void log(){
        System.out.println("111111");
    }

    @After("execution(public * com.example.springboot.controller.BodyController.*(..))")
    public void doAfter(){
        System.out.println("222222");
    }
}

(此时在BodyController的bodylist()中输出一句话:bodylist,验证方法与@Before与@After之间输出的先后顺序)
IDE返回顺序是 111111 bodylist 22222

注: 写代码时尽可能不要有重复的代码,此时发现@Before与@After中存在重复代码;
显得不专业,又在今后维护过程中麻烦;

HttpAspect 之AOP改造:

//演示段落

@Aspect
@Component //把文件引入到Spring容器中
public class HttpAspect {

    @Pointcut("execution(public * com.example.springboot.controller.BodyController.*(..))")
    public void log(){
    }
    
    @Before("log()")
    public void doBefore(){
        System.out.println("111111");
    }

    @After("log()")
    public void doAfter(){
        System.out.println("222222");
    }
}

除了System.out.println输出,还可以使用定义private final static Logger来记日志(输出);输出写logger.info()logger.error()

HttpAspect 之AOP改造和Log输出:

特别注意:定义Log时,括号内要写当前类名;且Log格式为 org.slf4j !!!

//采用段落

@Aspect
@Component //把文件引入到Spring容器中
public class HttpAspect {

    private final static Logger logger= LoggerFactory.getLogger(HttpAspect.class); //括号内要写当前类名;且Logger为org.slf4j

    @Pointcut("execution(public * com.example.springboot.controller.BodyController.*(..))")
    public void log(){
    }

    @Before("log()")
    public void doBefore(){
        logger.info("111111111111111111");
    }

    @After("log()")
    public void doAfter(){
        logger.info("222222222222222222");
    }
}

通过Console看得到:
Log输出比System.out.println多了时间记录等,更完善,所以更推荐采用这种方式输出;

—————————————
现在把@Before下的方法改下,记录HTTP请求
记录HTTP请求
记录HTTP请求
记录HTTP请求
记录HTTP请求
记录HTTP请求
记录HTTP请求
记录HTTP请求
记录HTTP请求

//就获取这些吧:

1.URL(请求路径)
2.请求method
3.ip
4.请求的类方法
5.参数

注:doBefore 构造器中引入JoinPoint 是为了输出“请求的类方法”和“参数”而配置

HttpAspect.java

@Pointcut("execution(public * com.example.springboot.controller.BodyController.*(..))")
public void log(){
}

@Before("log()")
public void doBefore(JoinPoint joinPoint){ //这里传入的 JoinPoint 是为了输出“请求的类方法”和“参数”而配置

    ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request=attributes.getRequest();
    //这里的HttpServletRequest格式为javax.servlet.http

    /**
     * URL(请求路径)
      */
    logger.info("url={}",request.getRequestURL());


    /**
     * 请求method
     */
    logger.info("method={}",request.getMethod());

    /**
     * ip
     */
    logger.info("ip={}",request.getRemoteAddr());

    /**
     * 请求的类方法 ——这里需要在doBefore方法属性中传入JoinPoint及实例
     */
    logger.info("class_method={}",joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName());
            //类名+"."+类方法

    /**
     * 参数
     */
    logger.info("args={}",joinPoint.getArgs());
 }

POSTMAN中请求GET 127.0.0.1:8081/springboot/body
IDE Console中返回如下:

url=http://127.0.0.1:8081/springboot/body
method=GET
ip=127.0.0.1
class_method=com.example.springboot.controller.BodyController.bodylist
args={} 

//因为url本身没有传参数,所以args部分为空

那么试一试POSTMAN中请求加个数字参数:GET 127.0.0.1:8081/springboot/body/2

POSTMAN返回:
{
    "id": 2,
    "size": "C",
    "age": 19
}

IDE Console返回包括:

args=2

—————————————
补充:

上面说到在POSTMAN中运行GET方法时,POSTMAN会返回信息,比如GET 127.0.0.1:8081/springboot/body/2

POSTMAN返回:
{
    "id": 2,
    "size": "C",
    "age": 19
}

若想在IDE Console中也看到这部分的返回(很多情况下需要这样),如何配置?
HttpAspect.java@After标签段落后添加 @AfterReturning 标签段落:
@AfterReturning
@AfterReturning
@AfterReturning

用于在IDE Console中返回POSTMAN中返回的内容,内容也许很多,但对于程序而言,他们都是 对象:Object
所以需要在doAfter方法构造器中引入Object实例对象

//在IDE Console中返回POSTMAN中返回内容的对象(无具体内容)

@AfterReturning(returning="object" ,pointcut = "log()")
public void doAfterReturning(Object object){
    logger.info("response={}",object);
}

试一试POSTMAN中请求:GET 127.0.0.1:8081/springboot/body/2

IDE Console中返回如下:

url=http://127.0.0.1:8081/springboot/body/2
method=GET
ip=127.0.0.1
class_method=com.example.springboot.controller.BodyController.bodyFindOne
args=2
。。。。。。
2222222222
response=com.example.springboot.domain

最后一行:response打印出了对象,但是还没打印出具体的内容。
——————————
若还也返回具体内容?
1.Body.java 中生成方法的toString()方法:

@Override
public String toString() {
    return "Body{" +
            "id=" + id +
            ", size='" + size + '\'' +
            ", age=" + age +
            '}';
}

2.HttpAspect.java@After标签下doAfterReturning方法中,logger.info返回处:object后添加.toString()

//在IDE Console中返回POSTMAN中返回内容的对象(添加了.toString(),返具体内容)

@AfterReturning(returning="object" ,pointcut = "log()")
public void doAfterReturning(Object object){
    logger.info("response={}",object.toString());
}

POSTMAN测试GET response=Body{id=2, size='C', age=19}

//IDE Console最后一行显示:

response=Body{id=2, size='C', age=19}

————————————————————————————

统一异常处理

为什么要 “统一” 异常处理?

假设有这么个情况:
当用POSTMAN运行POST方法,且少写了一个参数时,@Valid验证失败,IDE Console会抛出非常长串的异常信息;
比如没有上传size参数时,IDE Console会抛出空指针异常java.lang.NullPointerException:null

流程为:
因为size部分为空 →
输出语句中Object为None →
None的原因是 BodyController中 @Valid 拦截了上传 if语句return null→

BodyController中POST方法:

原代码如下:

@PostMapping(value="/body")
public Body bodyAdd(@Valid Body body, BindingResult bindingResult){
    if(bindingResult.hasErrors()){
        System.out.println(bindingResult.getFieldError().getDefaultMessage());
        return null; //发生错误就不能往下走了
    }
    body.setSize(body.getSize());
    body.setAge(body.getAge());

    return bodyRepository.save(body);
}

修改:

    @PostMapping(value="/body")
    public Object bodyAdd(@Valid Body body, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return bindingResult.getFieldError().getDefaultMessage();
            //return null; //发生错误就不能往下走了
        }
        body.setSize(body.getSize());
        body.setAge(body.getAge());

        return bodyRepository.save(body);
    }

如果缺失size,浏览器返回:size缺失;
如果参数完整,浏览器返回:一个上传材料的JSON文件;如:

{
  "id":79,
  "size":"8",
  "age":25
}

不过一个JSON文件格式太乱,不好交接给前端做接口;
我们希望它长这样:

//错误时

{
  "code":1,
  "msg":"size必传",
  "data":null
}
//正确时

{
  "code":0,
  "msg":"成功",
  "data":{
     "id":79,
     "size":"8",
     "age":25
  }
}

domain文件夹新建 Result.java

public class Result<T> {
    //错误码
    private Integer code;

    //提示信息
    private String msg;

    //具体的内容
    private T data;
}    
//。。。后面是SETTER&GETTER方法。。。

BodyControllerPOST方法改成:
(可以页面搜索观察下这个POST方法经过了哪一些改变)

    @PostMapping(value="/body")
    public Result<Body> bodyAdd(@Valid Body body, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            Result result=new Result();
            result.setCode(1);  //暂定为1,不用纠结
            result.setMsg(bindingResult.getFieldError().getDefaultMessage());
            //result.setData(null); //可以设置发生错误时返null,设不设置都一样
            return result;
        }
        body.setSize(body.getSize());
        body.setAge(body.getAge());

        Result result=new Result();
        result.setCode(0);
        result.setMsg("成功");
        result.setData(bodyRepository.save(body));
        return result;
    }

在这里插入图片描述
搞定
——————————————
回过头来看,代码还不够优雅:BodyController中的POST 代码重复太多
千万注意:代码优化不要等到以后再说!!以后 = NEVER,要改就现在改

新建Utils文件夹,新建ResultUtils.java

public class ResultUtils {
    
    //成功的话可能会引用到Object
    public static Result success(Object object){
        Result result = new Result();
        result.setCode(0);
        result.setMsg("成功");
        result.setData(object);
        return result;
    }

    //成功的话可能也应用不到Object
    public static Result success(){
        return success(null);
    }

    //失败时
    public static Result error(Integer code,String msg){
        Result result=new Result();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }
}

BodyControllerPOST方法再修改~~~~~~

    @PostMapping(value="/body")
    public Result<Body> bodyAdd(@Valid Body body, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return ResultUtils.error(1,bindingResult.getFieldError().getDefaultMessage());
        }
        body.setSize(body.getSize());
        body.setAge(body.getAge());

        return ResultUtils.success(bodyRepository.save(body));
    }

————————————————————————————

————————————————————————————

————————————————————————————

————————————————————————————

————————————————————————————

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值