SpringBoot目录
SpringBoot
面向spring开发与springboot的区别:之前使用spring开发,如果做一个简单的项目,需要自己动手导jar包或者maven导依赖整合其他框架,如SSM。但是使用springboot之后,可以快速的生成一个独立运行的spring项目以及主流框架的集成。总而言之,springboot就是简化spring应用的开发的框架,对spring技术栈的整合。
优点:
-
快速的创建一个可独立运行的Spring项目以及主流框架的集成。
-
使用嵌入式的Servlet容器,应用不需要打包成war包
-
starters自动依赖与版本控制。
springboot有很多的启动器,例如使用web功能或jdbc功能,只需要导入对应的启动器,则会自动导入所需要的jar包依赖以及对应的版本。 -
大量的自动配置,都有默认值,简化开发。可以通过配置文件更改默认值。
-
无序配置xml,无代码生成,开箱即用
-
准生成环境的运行时应用监控。
用于运维时监控项目的运行情况与状态。 -
与云计算的天然集成。
springboot入门容易,但如果想精通,需要先掌握spring底层原理。springboot就是在spring的基础上的再封装。
(1) 快速入门
微服务
单体服务:在微服务之前使用的一种架构思想。将所有的功能模块都打在一个war包中。
优点:
- 开发与测试简单
- 部署以及维护方便
- 拓展容易:当应用负载能力不足时,只需要多部署几个相同的war包即可。
微服务:一种架构思想。将每个功能模块独立出来,然后很具需求将各个功能元素进行动态组合部署到服务器中。如果有些功能模块使用频繁压力大,可以在其他服务器多部署。每一个元素都是可独立替换或独立升级的软件。
HelloWorld入门
-
创建maven项目
-
引入启动器
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>创建主程序
-
创建主程序编写一个主程序,添加*@SpringBootApplication*注解,main方法中 使用SpringApplication.run()方法。
@SpringBootApplication //用于标注主程序类,说明这是一个springBoot类应用。 public class HelloWorldMainApplication { public static void main(String[] args) { //让spring应用启动起来 SpringApplication.run(HelloWorldMainApplication.class,args); } }
-
编写相关的Controller或者service
@Controller public class HelloWorldController { @ResponseBody @RequestMapping("/hello") public String hello(){ return "Hello World!"; } }
-
启动主程序并访问网页
关于打jar包:
-
导入插件
<!--用于打包的插件,可以将一个应用打包成jar包--> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
-
通过MavenProjects控制台,使用package方法打包
-
打包成功。
-
使用控制台,启动jar包
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(i -
成功
Springboot入门详解
POM.xml文件
-
版本控制
-
pom.xml有一个<parent>,继承spring-boot-starter-parent
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> </parent>
-
点进spring-boot-starter-parent,然后发现它继承于spring-boot-dependencies
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>1.5.9.RELEASE</version> <relativePath>../../spring-boot-dependencies</relativePath> </parent>
-
在深入一步,就会发现这里面替我们管理好了各个jar包的版本。
-
-
启动器:导入jar包
pom.xml文件导入的依赖只有一个。那么关于web能正常运行的jar是如何导入的?
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
点进spring-boot-starter-web,就会发现这个依赖中,以及为我们导入了web功能模块相关的各种依赖。
spring-boot-starter:springboot的启动器,springboot有各个功能模块的启动器,例如web,jdbc,mail等,只需要相应的启动器,就可以导入该功能模块所需要的jar包.
主程序类
@SpringBootApplication用于标注主程序。
点进去进一步分析。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication{
可以发现该注解有**@SpringBootConfiguration和@EnableAutoConfiguration**两个注解
-
@SpringBootConfiguration
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration public @interface SpringBootConfiguration { }
是一个配置类,被@Configuration注释,spring底层表示被注释的类是一个配置类。
-
@EnableAutoConfiguration:自动配置
@AutoConfigurationPackage @Import({EnableAutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration {
发现被**@AutoConfigurationPackage和@Import({EnableAutoConfigurationImportSelector.class})**注释
- @AutoConfigurationPackage:自动配置包
@Import({Registrar.class}),被@Import修饰,表示导入Registrar.class,作用就是将主程序类所在包下的所有组件扫描到Spring容器中。 - @Import({EnableAutoConfigurationImportSelector.class}):用于自动导入该功能模块所需要的组件。
- @AutoConfigurationPackage:自动配置包
(2) 配置文件
- springboot使用一个全局配置文件。可以对一些默认配置值做一些修改
- application.properties 或
- application.yml。.yml是YAML语言文件,以数据为中心,比json、xml更适合做配置文件。
- 配置文件存放路径:
- src/main/resources包下
- 类路径下的config
本次以application.properties为例
server.port:可以修改当前端口号(默认为8080)。
使用配置文件给实体类赋值
例如:User实体类中有三个属性,id,username,password。
@Component("user")
public class User {
private int id;
private String username;
private String password;
}
在配置文件中配置属性。
user.id=1
user.username="思绪123"
user.password="666"
-
使用@Value注解
@Component("user") public class User { @Value("${user.id}") private int id; @Value("${user.username}") private String username; @Value("${user.password}") private String password; }
测试:
@RestController public class ConfigDemo { @Autowired private User user; @RequestMapping("/configTest") public User user(){ return user; } }
结果:
-
使用@ConfigurationProperties
首先需要导入依赖<dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-configuration-processor </artifactId> <optional> true </optional> </dependency>
在实体类上添加注解。@ConfigurationProperties(prefix = “user”):prefix用来指定前缀。一般前缀+属性名=配置文件的key。
@Component("user") @ConfigurationProperties(prefix = "user") public class User { private int id; private String username; private String password; }
测试结果:
多环境配置文件
实际开发中,一般有两个环境:开发环境和生产环境。
一般情况,我们在开发时会指定读取开发环境的配置,application-dev.propreties。
将项目部署到服务器上时,再去指定读取生产环境配置,application-pro.propreties。
例如,我在resources下配置了3个配置文件,它们的端口号不相同。
-
默认配置文件
当启动主程序时,默认读取的时application.properties。 -
指定开发环境
当我在主配置文件中配置spring.profiles.active=dev时,即此时启动的端口是:
-
指定生产环境
当配置为spring.profiles.active=pro时,
由此可知,我们可以在默认配置文件application.properties中根据不同环境,使用
spring.profiles.active来指定不同环境的配置文件。
(3) SpringBoot返回Json数据以及封装
对Json数据的处理
@RestController
SpringBoot返回json数据只需要在Controller上添加此注解即可。点进原码分析
@Controller
@ResponseBody
public @interface RestController
发现该注解由@Controller和@ResponseBody注解;@Controller用于标注该类是一个表现层的类,@ResponseBody用于返回json数据。
springboot对json数据解析框架是jackson。
默认对Json的处理
创建User实体类,用于测试。
//实体类
public class User {
private int id;
private String username;
private String password;
}
//省略getter和seter方法以及构造方法,之后都是如此。
编写Controller,分别用于测试返回User对象,List对象或Map对象。
@RestController
public class HelloWorldController {
@ResponseBody
@RequestMapping("/hello")
public String hello(){
return "Hello World!";
}
@RequestMapping("/user")
public User user(){
User user = new User(1, "思绪", "dawdwa");
return user;
}
@RequestMapping("/listUser")
public List<User> listUser(){
List<User> lists=new ArrayList<>();
lists.add(new User(1, "思绪", "dawdwa"));
lists.add(new User(1, "sixu", null));
lists.add(new User(1, null, "dawdwa"));
return lists;
}
@RequestMapping("/mapUser")
public Map<String,Object> mapUser(){
Map<String,Object> map=new HashMap<>();
map.put("作者1",new User(1, "思绪", "dawdwa"));
map.put("作者null",new User(1, null, "dawdwa"));
map.put("数值",999);
map.put("字符串","key值字符串");
return map;
}
}
结果:
-
User对象封装为json
-
List对象封装为json
-
Map对象封装为json
Json对null的处理
由上图结果所知,当结果为null时,返回的json数据也为null,如果希望json返回的数据用""来替代null,则自己配置jackson框架的配置类。
@Configuration
public class JacksonConfig {
@Bean
@Primary //用于指定默认注入bean对象。如果一个接口有多个实现类,则加此注解的类的对象为默认spring容器默认为接口引用注入的bean对象
@ConditionalOnMissingBean(ObjectMapper.class) //当不存在此bean对象时
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder){
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString("");
}
});
return objectMapper;
}
}
结果:原本的null被替换为了""
对Json数据的封装
实际开发中,除了封装数据之外,还要在返回的json中添加其他信息,例如状态码code,返回一些msg给调用者,让调用者可以根据这些信息进行逻辑判断。
-
实现需要定义同一的json格式,因为返回的数据类型不确定,所以需要使用到泛型。
public class JsonResult<T> { private int code; //状态码 private T date; //返回的数据 private String msg; //没有数据返回,默认状态码为0,提示信息为“成功” public JsonResult() { this.code=0; this.msg="成功"; } //没有数据返回,人为指定状态码和提示信息 public JsonResult(int code, String msg) { this.code = code; this.msg = msg; } //有数据返回,默认状态码为0,提示信息为“成功” public JsonResult(T date) { this.code = 0; this.date = date; this.msg = "成功"; } //有数据返回,默认状态码为0,认位指定成功信息 public JsonResult(T date,String msg) { this.code = 0; this.date = date; this.msg = msg; }
-
编写Controller
@RestController public class HelloWorldController { //没有传入信息,默认状态码与msg @RequestMapping("/jsonUser01") public JsonResult<User> JsonUser01() { JsonResult<User> userJsonResult = new JsonResult<>(); return userJsonResult; } //没有传入信息,人为指定状态码与msg @RequestMapping("/jsonUser03") public JsonResult<User> JsonUser03() { return new JsonResult<>(999, "人为指定"); } //有数据返回 @RequestMapping("/jsonUser02") public JsonResult<User> JsonUser02() { return new JsonResult<>(new User(1, "思绪", "132")); } //有数据返回,人为指定msg @RequestMapping("/jsonListUser02") public JsonResult<List> JsonListUser() { ArrayList<User> users = new ArrayList<>(); users.add(new User(1, null, null)); users.add(new User(2, "思绪", "555")); return new JsonResult<>(users, "传入list数据,人为指定msg"); } }
-
结果测试
(4) SpringBoot的日志
slf4j
slf4j并不是一个具体的体制解决方案,只是服务于各种各样的之日志系统。
就是只需要用统一的方式写记录日志的代码,而不需要关心日志是通过什么日志系统以什么风格输出的。
例如,如果在项目中使用了 slf4j 记录日 志,并且绑定了 log4j(即导入相应的依赖),则日志会以 log4j 的风格输出;后期需要改为以 logback 的风格输出日志,只需要将 log4j 替换成 logback 即可,不用修改项目中的代码。
springboot默认使用的就是slf4j+logback,而我们实际项目中使用的就是这种方式来输出日志,效率挺高的。
如何使用slf4j?直接使用 LoggerFactory 创建即可。
@SpringBootTest
class DemoApplicationTests {
Logger logger = LoggerFactory.getLogger(getClass());
@Test
void contextLoads() {
logger.trace("这是----trace");
logger.debug("这是----debug");
logger.info("这是----info");
logger.warn("这是----warn");
logger.error("这是----error");
}
}
结果:
日志级别:trace<debug<info<warn<error
springBoot默认为info级别,只有比info级别高的日志才能输出。
在配置文件中,使用logging.level可以更改日志输出级别。例如logging.level.com.example=trace,则com.example包下的日志输出级别为trace。
日志的配置
logging.file.name:可以指定路径和log文件的名字
logging.file.path:只可以指定log的路径, 不能指定log的名字, log文件的名字默认为spring.log。二者只可生效一个
logging.pattern.file:设置日志的输出格式。
logging.pattern.file=%d{yyyy/MM/dd} === [%thread] === %-5level === %logger{50} === %msg%n
%d{yyyy/MM/dd}:表示日期格式
[%thread]:线程名称
%-5level :日志级别,从左显示5个字符宽度
%logger{50} : logger 名 字长36个字符
%msg:日志信息
%n:换行
logging.file.max-size:指定日志的最大大小。默认为10M。超过指定大小会被压缩为.gz的压缩文件,文件名逐渐累积。
logging.file.max-history:指定日志保存天数。默认7天。
logging.file.clean-history-on-start:应用启动时强制清除日志。默认为false。
(5) 整合MVC
注解
@RestController ,包含了@Controller和@ResponseBody ,@ResponseBody 注解是将返回 的数据结构转换为 Json 格式。
@RequestMapping 是一个用来处理请求地址映射的注解,它可以用于类上,也可以用于方法上。在类 的级别上的注解会将一个特定请求或者请求模式映射到一个控制器之上,表示类中的所有响应请求的方 法都是以该地址作为父路径;在方法的级别表示进一步指定到处理方法的映射关系。
该注解有6个属性,一般在项目中比较常用的有三个属性:value、method 和 produces。
- value 属性:指定请求的实际地址,value 可以省略不写
- method 属性:指定请求的类型,主要有 GET、PUT、POST、DELETE,默认为 GET
- produces属性:指定返回内容类型,如 produces = “application/json; charset=UTF-8”
@RequestBody :注解用于接收前端传来的实体,接收参数也是对应的实体类的属性。
@RequestParam :获取请求参数的。url为: http://localhost:8080/user?id=1 。
如果url的id与参数的形参不一样,则需要指定给注解指定。
例如:当url为这种: http://localhost:8080/user?iddd=1
@RequestMapping("/user")
public String testRequestParam(@RequestParam(value = "iddd", required = false) Integer id) { System.out.println("获取到的id为:" + id);
return "success";
}
@PathVariable也是获取参数的,但是它支持的是持 restfull 风格的 url,例如:http://localhost:8080/user/{id}。如果占位符与方法中的形参不一致的话,也需要在注解中指定。
访问静态资源
直接通过网址即可访问。
例如:
-
在resources/static下放
-
直接通过url地址即可访问:
配置拦截器
-
首先,自定义一个拦截器,继承HandlerInterceptor接口,实现其内部的三个方法:前置方法,后置方法,完成方法。
@Component("interceptor") public class MyInterceptor implements HandlerInterceptor{ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("自定义拦截器前置方法"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("自定义拦截器前置方法"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("自定义拦截器完成方法"); } }
-
由于springBoot没有配置文件,所以需要定义一个配置类。实现WebMvcConfigurer,实现其中的添加拦截器方法addInterceptor();
/*出自定义配置类*/ @Configuration public class MyConfig implements WebMvcConfigurer { @Resource(name = "interceptor") private MyInterceptor interceptor; //实现方法,添加拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(interceptor).addPathPatterns("/**"); } }
addInterceptor():添加自定义的拦截器对象
addPathPatterns():添加要就拦截的路径。/**表示拦截全部路径
excludePathPatterns():添加所有白名单路径,不拦截。 -
测试结果。
监听器
什么是 web 监听器?web 监听器是一种 Servlet 中特殊的类,它们能帮助开发者监听 web 中特定的事 件,比如 ServletContext, HttpSession, ServletRequest 的创建和销毁;变量的创建、销毁和修改等。 可以在某些动作前后增加处理,实现监控。
SpringBoot中监听器的使用
web 监听器的使用场景很多,比如监听 servlet 上下文用来初始化一些数据、监听 http session 用来获 取当前在线的人数、监听客户端请求的 servlet request 对象来获取用户的访问信息等等。这一节中, 我们主要通过这三个实际的使用场景来学习一下 Spring Boot 中监听器的使用。
监听servlet上下文对象
监听 servlet 上下文对象可以用来初始化数据,用于缓存。
什么意思呢?我举一个很常见的场景,比如 用户在点击某个站点的首页时,一般都会展现出首页的一些信息,而这些信息基本上或者大部分时间都 保持不变的,但是这些信息都是来自数据库。如果用户的每次点击,都要从数据库中去获取数据的话, 用户量少还可以接受,如果用户量非常大的话,这对数据库也是一笔很大的开销。
针对这种首页数据,大部分都不常更新的话,我们完全可以把它们缓存起来,每次用户点击的时候,我 们都直接从缓存中拿,这样既可以提高首页的访问速度,又可以降低服务器的压力。如果做的更加灵活 一点,可以再加个定时器,定期的来更新这个首页缓存。就类似与 CSDN 个人博客首页中排名的变化一 样。
-
首先写一个demo,用于模拟service层服务.
@Service public class UserService { //模拟service层,从数据库获取用户信息。 public User getUser(){ return new User("思绪","123"); } }
-
写一个监听器,实现功能将service查询的用户存入到域对象中。这样前端请求用户信息时,可以直接从application域中获取,而不用频繁的访问数据库。该监听器需要实现 ApplicationListener<ContextRefreshedEvent> 接口,重写 onApplicationEvent 方法,将 ContextRefreshedEvent 对象传进去。
//自定义监听器 @Component public class MyServletContextListener implements ApplicationListener<ContextRefreshedEvent>{ @Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { //1.获得spring的上下文环境 ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext(); //2.获得service并获得其从数据库中读取的用户对象 UserService UserService = applicationContext.getBean(UserService.class); User user = UserService.getUser(); //3.获取域对象,然后将数据存入域对象中 ServletContext application = applicationContext.getBean(ServletContext.class); application.setAttribute("user",user); } }
-
写一个controller层测试监听器。
@RestController @RequestMapping("listener") public class TestListener { @GetMapping("test") public User test(HttpServletRequest request){ return (User) request.getServletContext().getAttribute("user"); } }
-
测试:
监听HTTP会话 Session对象
监听器还有一个比较常用的地方就是用来监听 session 对象,来获取在线用户数量,现在有很多开发者 都有自己的网站,监听 session 来获取当前在线用户数量是个很常见的使用场景,下面来介绍一下如何来使用。
-
创建一个监听器。先该监听器需要实现 HttpSessionListener 接口,然后重写 sessionCreated 和 sessionDestroyed 方法
@Component public class MyHttpSessionListener implements HttpSessionListener{ private static final Logger logger= LoggerFactory.getLogger(MyHttpSessionListener.class); public Integer count=0; @Override public void sessionCreated(HttpSessionEvent se) { count++; logger.info("用户上线"); se.getSession().getServletContext().setAttribute("count",count); } @Override public void sessionDestroyed(HttpSessionEvent se) { count--; logger.info("用户下线"); se.getSession().getServletContext().setAttribute("count",count); } }
-
在controller层写一个方法用于测试。
@GetMapping("getCount") public String getCount(HttpServletRequest request, HttpServletResponse response){ Integer count=(Integer) request.getSession().getServletContext().getAttribute("count"); return "当前在线人数:"+count; }
-
结果测试:
注意:由于用户关闭浏览器的动作并不会告知服务器,因此当用户关掉浏览器的时候,并不会调用监听器的sessionDestroyed方法。
这就造成了一个问题:当两个甲、乙用户打开浏览器,此时在线人数为2,甲关掉浏览器,此时正确的在线人数应该为1;但是由于服务器不知道甲关闭了浏览器,所以并没有执行sessionDestroyed方法。
sessionDestroyed执行有两个条件
- 根据session设置的时长,等待session的过期。时间到则自动销毁。
- 调用session.invalidate()方法。
监听客户端请求Servlet Request对象
使用监听器获取用户的访问信息。监听器实现 ServletRequestListener 接口即可,然后通过 request 对象获取一些信息。
-
监听器
@Component public class MyServletRequestListener implements ServletRequestListener { private static final Logger logger= LoggerFactory.getLogger(MyHttpSessionListener.class); @Override public void requestDestroyed(ServletRequestEvent sre) { HttpServletRequest servletRequest = (HttpServletRequest) sre.getServletRequest(); logger.info("请求的url:{}",servletRequest.getRequestURL()); logger.info("session的id:{}",servletRequest.getRequestedSessionId()); User user =(User) sre.getServletContext().getAttribute("user"); logger.info("user:{}",user); } //忽略 @Override public void requestInitialized(ServletRequestEvent sre) { } }
-
controller层测试方法
@GetMapping("requestListener") public String requestListener(){ return "success"; }
-
测试结果:
(6) 整合Mybatis
入门步骤
-
导入mybatis和jdbc的启动器,以及Mysql数据库是的依赖。
mybatis的启动器中已经整合了jdbc的启动器。 -
再配置文件中配置数据源信息,springboot会自动配置数据库驱动,只需要配置url,username和password。
注意:踩过的坑!!!配置数据库url的时候,一定要配置时区,否则可能会报错。
例如:spring.datasource.url=jdbc:mysql:///test01?serverTimezone=Asia/Shanghai3.写一个与数据库对应的实体类,数据访问层dao层,服务层service,以及Controller层。
代码层次图:
-
实体类:
public class Student { private int id; private String name; }//忽略getset方法以及构造方法
-
dao层
@Repository public interface StudentMapper { //根据id查找学生姓名 @Select("select student.name from student where id=#{id}") public String findNameById(int id); //根据id查找学生 @Select("select * from student where id=#{id}") public Student findStudentById(int id); }
-
service层
@Service public class StudentService { @Resource private StudentMapper studentMapper; public String findNameById(int id){ return studentMapper.findNameById(id); } public Student findStudentById(int id){ return studentMapper.findStudentById(id); } }
-
controller层
@RestController public class studentController { @Resource private StudentService studentService; @RequestMapping("findNameById/{id}") public String findNameById(@PathVariable int id){ return studentService.findNameById(id); } @RequestMapping("findStudentById/{id}") public Student findStudentById(@PathVariable int id){ return studentService.findStudentById(id); } @RequestMapping("test") public String test(){ return "测试"; } }
-
结果测试
-
配置事务
配置事务非常简单,只需要再servicecg添加**@Transactional**注解即可。
例如:添加一个方法,addStudent(String name);
-
持久层
@Insert("insert into student(name) values(#{name}) ") public void addStudent(String name);
-
controller层
@RequestMapping("/addStudent/{name}") public void addStudent(@PathVariable String name){ studentService.addStudent(name); }
-
服务层
- 当没有事务控制时,给服务层该方法手动添加一个异常
//@Transactional(rollbackFor = Exception.class) public void addStudent(String name){ studentMapper.addStudent(name); //制造异常 int a=10/0; }
则:
-
当有异常,添加事务控制注解时。
@Transactional(rollbackFor = Exception.class) public void addStudent(String name){ studentMapper.addStudent(name); //制造异常 int a=10/0; }
结果:
注意:当程序抛出非运行时异常时,可能导致该注解作用失效。使用@Transactional的rollbackFor属性指定异常即可解决,切记!!!
事务的范围
有坑注意:用一个demo说明。
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public synchronized void addUser(User user) {
// 实际中的具体业务……
userMapper.addUser(user);
}
}
会出现的问题:假如数据库不允许同一个添加重复用户名的用户,那么每次添加前都会比对数据库有没有重复用户名的用户。于是为了考虑多线程,给方法加了一个锁。但是并没有起效果。
从上面方法中可以看到,方法上是加了事务的,那么也就是说,在执行该方法开始时,事务启动,执行 完了后,事务关闭。但是 synchronized 没有起作用,其实根本原因是因为事务的范围比锁的范围大。 也就是说,在加锁的那部分代码执行完之后,锁释放掉了,但是事务还没结束,此时另一个线程进来 了,事务没结束的话,第二个线程进来时,数据库的状态和第一个线程刚进来是一样的。即由于mysql Innodb引擎的默认隔离级别是可重复读(在同一个事务里,SELECT的结果是事务开始时时间点的状 态),线程二事务开始的时候,线程一还没提交完成,导致读取的数据还没更新。第二个线程也做了插 入动作,导致了脏数据。
图解释:
**原因:**就是由于事务的范围比锁大
**解决方法:**在调用service层的那个方法(controller层)开启同步,让锁的范围比事务大即可。
(7) SpringBoot中的异常处理
在项目开发过程中,不管是对底层数据库的操作过程,还是业务层的处理过程,还是控制层的处理过 程,都不可避免会遇到各种可预知的、不可预知的异常需要处理。而处理异常最常用的就是全局处理异常的方式。
全局异常处理
全局异常处理的方法是: @ControllerAdvice+@ExceptionHandler
-
首先定义返回的统一json结构,为例简单示范,仅有code和msg两种信息。
//定义统一的返回数据格式 public class JsonResult { private String code; private String msg; public JsonResult(String code, String msg) { this.code = code; this.msg = msg; } public JsonResult() { this.code="200"; this.msg="操作成功"; } }
-
新建一个 GlobalExceptionHandler 全局异常处理类,然后加上 @ControllerAdvice 注解即可拦截项 目中抛出的异常。
@ControllerAdvice :包含了 @Component ,则springBoot启动时,会作为组件交给sping管理。还有个 basePackages 属性,该属性是用来拦截哪个包中的异常信息,但一般不指定,用于拦截全局异常。
@ControllerAdvice @ResponseBody public class GlobalException { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(GlobalException.class); }
-
如何使用全局异常类?只需要在全局异常类中定义方法,使用**@ExceptionHandler**指定的异常,然后在方法中出处理异常,最后将结果通过统一的json结构返回给调用者即可。
-
例如:以处理参数缺失异常为例
在全局异常处理类添加方法@ExceptionHandler(MissingServletRequestParameterException.class) @ResponseStatus(value = HttpStatus.BAD_REQUEST) public JsonResult handleHttpMessageNotReadable(MissingServletRequestParameterException ex) { logger.error("请求缺少必要参数:{}", ex.getMessage()); return new JsonResult("400", "缺少必要参数"); }
如代码所示,当出现请求参数缺失异常时,输出日志信息,并返回给调用者一个自定义的JsonResult。
Controller层代码:@RestController public class ExceptionController { private static final Logger logger= LoggerFactory.getLogger(ExceptionController.class); @RequestMapping("/exception") public JsonResult exception(@RequestParam("name") String name, @RequestParam("password") String password){ logger.info("name:{}",name); logger.info("password:{}",password); return new JsonResult(); } }
如代码所示,此方法需要传入name和password两个参数。
开始测试:-
正常传入参数:
-
输入参数缺失:
-
-
拦截Exception异常
由于异常有各种各样,种类繁多。但是Exception异常是父类,因此可以直接拦截指定 Exception 异常,一劳永逸。
在全局异常处理类中定义方法:
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleUnexpectedServer(Exception ex) {
logger.error("系统异常:", ex);
return new JsonResult("500", "系统发生异常,请联系管理员");
}
注意:直接拦截Exception类虽然方便,但是不利于定位。因此项目中都是拦截一些具体类型的异常。把拦截Exception异常的方法放在最后,用于拦截另程序员意想不到的异常。
自定义异常类
实际项目中,除了拦截一些系统异常外,在某些业务上,我们需要自定义一些业务异常。例如在微服务中,服务调用失败或超时等,需要调用一个自定义的异常,当出现这类异常抛出后,由GlobalException处理。
-
定义异常信息:由于在业务中,有很多异常,针对不同的业务,可能给出的提示信息不同,所以为了方便项目异常信息 管理,我们一般会定义一个异常信息枚举。
//用于定义业务可能出现的异常信息 public enum ExceptionEnum { /** 参数异常 */ PARMETER_EXCEPTION("102", "参数异常!"), /** 等待超时 */ SERVICE_TIME_OUT("103", "服务调用超时!"), /** 参数过大 */ PARMETER_BIG_EXCEPTION("102", "输入的图片数量不能超过50张!"), /** 500 : 一劳永逸的提示也可以在这定义 */ UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!"); private String code; private String msg; ExceptionEnum(String code, String msg) { this.code = code; this.msg = msg; } //get,set方法
-
定义异常
public class BusinessErrorException extends RuntimeException { private String code; private String msg; public BusinessErrorException(ExceptionEnum exceptionEnum){ this.code=exceptionEnum.getCode(); this.msg=exceptionEnum.getMsg(); } //get,set }
-
在全局异常处理类中添加方法,处理自定义异常。
//处理自定义异常 @ExceptionHandler(BusinessErrorException.class) @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public JsonResult handleBusinessError(BusinessErrorException bus){ logger.error("code:",bus.getCode()); logger.error("msg:",bus.getMsg()); return new JsonResult(bus.getCode(),bus.getMsg()); }
-
在controller层自定义一个方法测试。
//测试自定义异常 @RequestMapping("/test") public JsonResult test(){ try { int a=1/0; }catch (Exception e){ throw new BusinessErrorException(ExceptionEnum.UNEXPECTED_EXCEPTION); //抛出枚举中的系统异常 } return new JsonResult(); }
-
结果:
(8) SpringBoot的AOP
关于AOP的概念以及各种专有名词的含义请见Spring笔记AOP部分。
**导入依赖:**使用AOP,首先要AOP的启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
自定义controller层,用于测试:
@RestController
public class AopController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping("test")
public String test(){
logger.info("controller层的方法在执行=========");
return "ok";
}
}
实现AOP切面:定义一个类,在类上添加@Aspect注解。此类用于定义通知方法。
@Aspect
@Component
public class LogAscept {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
//以输出日志的方式测试AOP注解
}
**@Pointcut:**用于定义切入点
@Pointcut("execution(* com.example.demo.controller..*.* (..))") //表示controller层的所有方法
public void pointCut(){}
**@Before:**用于定义前置通知,在方法执行之前执行。参数是定义的切入点方法名称。
@Before("pointCut()")
public void doBefore(JoinPoint joinPoint){
logger.info("前置通知开始执行");
Signature signature = joinPoint.getSignature(); //获取签名
String declaringTypeName = signature.getDeclaringTypeName(); //获取方法所在包名
String name = signature.getName(); //获取方法名
logger.info("包名:{},方法名:{}",declaringTypeName,name);
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String url = request.getRequestURL().toString();
String id = request.getRemoteAddr();
logger.info("url:{} id:{}",url,id);
}
测试结果:
**@After :**当切入点方法执行完毕后执行。
@After("pointCut()")
public void doAfter(){
logger.info("后置通知执行");
}
测试结果:
@AfterReturning:与@After类似,都是在切入点方法执行后执行。不同的是可以获取切入点方法的返回值。
@AfterReturning(pointcut = "pointCut()",returning = "result")
public void doAfterReturn(Object result){
logger.info("AfterReturn()方法执行,切入点方法返回值是:{}",result);
}
测试结果:
**@AfterThrowing:**在切入点方法执行后执行,可以接受该方法抛出的异常。
@AfterThrowing(pointcut = "pointCut()",throwing = "exce")
public void doAfterThrowing(Throwable exce){
logger.info("出现异常的原因:{}",exce.getMessage());
}
测试结果:
由此可知:@AfterReturning和@AfterThrowing只能同时执行一个
正常执行时,只会执行@AfterReturning
出现异常时,只会执行@AfterThrowing
(9) Springboot集成swagger
为何要使用swagger?
随着互联网的发展,目前呈现出前后端分离的态势。于是前端与后端的唯一联系,就变成了API接口。但是由于代码更新,后端人员需要花费不少的精力去更新API,而前端也经常会遇到API接口文档于是实际情况不同。而swagger就可以解决这个问题。
swagger的入门?
-
导入swagger2的依赖
<!--导入swagger依赖--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>
注意:踩过的坑,不同版本的springboot使用swagger,要使用对应版本。
例如:2.3.5的springboot使用2.2.2的swagger2就会有以下问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2D6tRrqG-1604206704017)(/1604199609686.png)] -
配置swagger配置类
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) //指定构建api文档的方法 .apiInfo(apiInfo()) .select() //指定哪些路径要生成api文档 .apis(RequestHandlerSelectors.basePackage("com.example.demo.controller")) .paths(PathSelectors.any()) .build(); } /*构建文档详细信息*/ private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("这是页面标题") .description("这是接口描述") .contact("这是联系方式") .version("这是版本号") .build(); } }
注解:
@EnableSwagger2:顾名思义,开启swagger2。 -
测试:启动后在网站输入http://localhost:8080/swagger-ui.html
swagger的详细使用
-
实体类注解
@ApiModel("学生实体类") public class Student { @ApiModelProperty("学生唯一的id") private int id; @ApiModelProperty("学生的姓名") private String name; }
@ApiModel: 注解用于实体类,表示对类进行说明
@ApiModelProperty: 注解用于类中属性 -
Controller层中注解
@RestController @Api("controller层") public class StudentController { @Resource(name = "studentService") @ApiModelProperty("service层对象") private StudentService studentService; @GetMapping("searchUserById/{id}") @ApiOperation("根据id获得指定唯一学生") public Student searchUserById(@PathVariable @ApiParam("参数:学生id") Integer id) { return studentService.searchUserById(id); } }
@Api:用于注解在类上,表示该类时swagger的资源。
@ApiOperation: 注解用于方法,表示一个 http 请求的操作。
@ApiParam:注解用于参数上,用来标明参数信息。 -
测试:启动后在网站输入http://localhost:8080/swagger-ui.html