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();
}
}
(当然,此项目中.yml
的server: 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尾端随意输入一个数字,如55
:http://localhost:8081/hello/say/55
浏览器渲染
id:55
value也可以写成value="/{id}/say"
;
同时URL也变了,如55
:http://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尾端随意输入一个数字,如66
:http://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还有一个作用,可以在括号中规定id
、required
、defaultValue
;(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,还要更新其他参数,这里是size
和age
;
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入数据库。
测试时间,我们可以这样安排:
先把数据库中数据清空,以便思路清晰;
把size
的length
改为1
,然后把BodyService中的body2
size改成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方法。。。
BodyController中POST方法改成:
(可以页面搜索观察下这个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;
}
}
BodyController中POST方法再修改~~~~~~
@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));
}
————————————————————————————
————————————————————————————
————————————————————————————
————————————————————————————
————————————————————————————