SpringBoot深入浅出
SpringBoot项目初始化
idea提供了完整的springboot项目初始化流程,唯一问题可能出在没有翻墙的情况下,无法从springboot官网下载配置。有多种解决方案:
- 可以从官网上下载项目后导入idea。
- 可以使用aliyun
- 对idea进行设置
@SpringBootApplication注解
@SpringBootApplication是一个组合注解:
为@SpringBootConfiguration, @EnableAutoConfiguration,@ComponentScan的组合
其默认的扫描范围为启动类所在的包及其子包
@Configuration和@EnableAutoConfiguration
@Configuration可以将一个类注册成配置类,相当于xml中的<beans/>结点,可以在其中配置bean
@Configuration //作为配置类,由于启动类有@EnableAutoConfiguration,该配置类会被加载到容器中
public class C {
@Bean("user")//注册一个Bean并赋予name,如果没有给name,则默认为方法名+首字母小写。
public User getUser(){
return new User(1L,"s",SexEnum.MALE,"n");
}
}
在一般spring中,该类相当于一个配置文件,可以主动读取后,初始化IOC容器,然后获取Bean。存在多个配置文件,可能会需要整合到一起。
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(C.class);
User user = (User) ctx.getBean("user");
但是在SpringBoot中由于@EnableAutoConfiguration的存在,所有配置类会被整合到一起,所以不用手动去加载其余的配置类。
@Bean
在配置类中使用,加在方法上表明注入一个Bean。(name默认为方法名,也可以指定name)
@Bean
public User user(){
return new User()
}
当要注入的Bean依赖其他对象时,将该对象方法在方法参数里,容器会进行自动注入(前提是有该类型的Bean)
@Bean
public User user(Toy toy){//Toy被声明为Bean
return new User(toy);
}
在配置类中,使用了加上@Bean的方法,不会再次调用该方法,而是直接从容其中获取Bean。
修改默认版本号
SpringBoot默认配置了许多依赖的版本号,所以我们引入依赖时不用加版本号。
可以引入依赖时指定版本号来覆盖默认版本号,还可以在pom.xml中配置版本号如下:
<properties>
<mysql.version>5.1.43</mysql.version>
</properties>
测试SpringMVC页面跳转
一、配置webapp
jsp跳转必须跳到webapp目录下,注意,这个目录是需要配置的。
1、新建web模块
2、设置webapp目录为web模块
双击红框处
选择建好的webapp目录(该目录要和java、resources文件夹同级别)
项目结构:
二、添加对应jar
<!-- tomcat支持 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!-- jstl标签库 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
三、创建Controller、配置文件、新建jsp
@Controller
public class IndexController {
@RequestMapping("/index")
public String index(){
System.out.println("hh");
return "index";
}
}
#springMVC jsp跳转到webapp目录下
spring.mvc.view.prefix=/jsp/
spring.mvc.view.suffix=.jsp
配置thymeleaf跳转
导入依赖
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
编写页面
如何去访问?查看Thymeleaf配置类,我们可以看见已经配置了前后缀!
编写控制器:
启动项目,在浏览器输入http://localhost:8080/index即可访问。
测试IOC
- 配置扫描
@SpringBootApplication
@ComponentScan(basePackages = {"com.example.springboot2"}) //有了上面的注解,可以不用加
public class Springboot2Application {
public static void main(String[] args) {
SpringApplication.run(Springboot2Application.class, args);
}
}
- 以注解方式配置bean
public interface IndexService {
void doService();
}
@Service
public class IndexServiceImpl implements IndexService {
@Override
public void doService() {
System.out.println("doService");
}
}
- 注入bean并使用
@RequestMapping("/testIOC")
public String testIOC(){
indexService.doService();
return "index";
}
书中讲了很多使用配置类来代替配置文件。
使用配置文件
可以在默认的application.properties中进行自定义配置,并且读取
- 添加自定义配置
# 自定义属性配置
database.driverName=com.jdbc.Driver
database.url = jdbc:mysql://localhost:3306/springboot
database.username = root
database.password = 123456
- 使用@Value注解导入配置
@Component
public class DataBaseProperties {
@Value("${database.driverName}")
private String driverName;
@Value("${database.url}")
private String url;
private String username;
private String password;
@Value("${database.username}")
public void setUsername(String username) {
this.username = username;
}
@Value("${database.password}")
public void setPassword(String password) {
this.password = password;
}
}
配置文件切换
在application.properties中就可以指定使用哪个配置文件
spring.profiles.active=dev
SpringBootAOP
只需要导入aop jar包即可,不需要进行其他配置!
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
整合Mybatis
偏向于使用注解来代替XML配置文件,也可以在配置文件中声明mybatis.xml文件的位置
从而使用xml文件配置mybatis。
- 引入数据源和mybatis jar包 和 对数据源进行配置(这里使用了DBCP数据源)。
<!--datasource-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
</dependency>
<!--配置mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
# 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_library
spring.datasource.username=root
spring.datasource.password=5808074wujiajun
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource
# 最大等待连接中的数量,0为没有限制
spring.datasource.dbcp2.max-idle=10
# 最大连接活动数
spring.datasource.dbcp2.max-total=50
# 最大等待毫秒数,单位为ms,超过时间会出错误信息
spring.datasource.dbcp2.max-wait-millis=10000
# 初始化连接数
spring.datasource.dbcp2.initial-size=5
- 编写pojo类、mapper.xml和接口文件
@Alias("user") //mybatis配置别名
public class User {
private Long id;
private String userName;
private SexEnum sex;
private String note;
// getter setter......
}
<?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.example.springboot2.dao.UserMapper">
<select id="getUser" parameterType="long" resultType="user">
select id,user_name as userName,sex,note from t_user where id = #{id}
</select>
</mapper>
public interface UserMapper {
User getUser(long id);
}
- 注解配置
//启动类注解
@SpringBootApplication
@MapperScan(basePackages = {"com.example.springboot2.dao"}) //接口文件扫描
public class Springboot2Application {
public static void main(String[] args) {
SpringApplication.run(Springboot2Application.class, args);
}
}
# mybatis配置
mybatis.mapper-locations=classpath:mappers/*.xml
# mybatis扫描别名包,和注解@Alias联用
mybatis.type-aliases-package=com.example.springboot2.pojo
# 配置typeHandler的扫描包
mybatis.type-handlers-package=com.example.springboot2.utils
# mybatis配置日志打印sql
logging.level.com.example.springboot2.dao=DEBUG
整合Druid数据源
1、引入依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
2、添加配置
spring:
datasource:
username: root
password: 5808074wujiajun
url: jdbc:mysql://localhost:3306/springboot_library
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#druid自定义配置
initialSize: 5
minIdle: 5
maxActive: 15
maxWait: 60000
timeBetweenEvictionRunsMillis: 2000
minEvictableIdleTimeMillis: 600000
maxEvictableIdleTimeMillis: 900000
validationQuery: select 1
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
keepAlive: true
phyMaxUseCount: 100000
filters: stat
3、自定义配置类读取我们写的配置
@Configuration
public class DruidConfiguration {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
return dataSource;
}
@Bean
public ServletRegistrationBean druidStatViewServlet() {
ServletRegistrationBean servletRegistrationBean
= new ServletRegistrationBean(new StatViewServlet(),
"/druid/*");
servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
servletRegistrationBean.addInitParameter("loginUsername", "admin");
servletRegistrationBean.addInitParameter("loginPassword", "123456");
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean druidStatFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions",
"*.js,*.gif,*.jpg,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
开启时事务(注解形式)
(在mybatis整合完成的基础上继续配置)虽然事务管理使用了AOP,但是不需要导入aop的jar包。
在Service实现类上或方法上可以添加@Transactional注解,来为整个类或单个方法添加事务管理。
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
有两个属性:隔离级别和传播行为。隔离级别和传播行为可以去看SpringBoot深入浅出的具体解释。
这里想谈一下传播行为。何为传播行为:在Spring中,当一个方法调用另一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为。
@Transactional失效。当一个Service中的方法A使用this调用另一个同在service中的方法B时(称为自调用),B的事务会失效。
Spring数据库事务由AOP实现,而AOP的原理事动态代理,在自调用的过程中,是类自身的调用,
那么不会产生AOP,这样Spring就不能将你的代码织入到约定流程中。可以使用一个Service调用另一个Service来解决。
使用Redis
配置
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.4.0.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
# redis配置
# 配置连接池
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-wait=2000ms
# 配置Redis服务器
spring.redis.port=6397
spring.redis.host=127.0.0.1
spring.redis.password=123456
# redis连接超时时间 ms
spring.redis.timeout=1000ms
使用模板类操作redis
@Autowired
private RedisTemplate redisTemplate = null;
@Autowired
private StringRedisTemplate stringRedisTemplate = null;
@RequestMapping("/stringAndHash")
@ResponseBody
public Map<String,Object> test(){
redisTemplate.opsForValue().set("key1","value");
redisTemplate.opsForValue().set("int_key","1");
stringRedisTemplate.opsForValue().set("int","1");
stringRedisTemplate.opsForValue().increment("int");
Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
//减一操作
jedis.decr("int");
HashMap<String, String> hash = new HashMap<>();
hash.put("field1","value1");
hash.put("field2","value2");
//存入散列数据类型
stringRedisTemplate.opsForHash().putAll("hash",hash);
//新增字段
stringRedisTemplate.opsForHash().put("hash","field3","value3");
//绑定散列操作的key,这样可以连续对同一个散列数据类型进行操作
//这里专门针对hash做操作。
BoundHashOperations<String, Object, Object> boundHashOps = stringRedisTemplate.boundHashOps("hash");
//删除两个字段
boundHashOps.delete("field1","field3");
//新增字段
boundHashOps.put("filed4","value5");
HashMap<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
开启Redis缓存
- 为启动类添加@EnableCaching//开起缓存机制
- 配置文件
# redis缓存管理器配置
spring.cache.type=REDIS
spring.cache.cache-names=redisCache
#是否允许缓存空值
spring.cache.redis.cache-null-values=true
#缓存超时时间,0为不设置
spring.cache.redis.time-to-live=0ms
- 使用注解开启缓存
@CachePut:表示将方法结果返回缓存中
@Cacheable:表示先从缓存中通过定义的键查询,如果可以查询数据,则返回;否则执行该方法,并将返回结果保存至缓存中
@CacheEvict:通过定义的键移除缓存
具体示例可见书。缓存机制是使用AOP实现的。
SpringMVC
数据类型转化
简单一对一转化
@Component
public class String2UserConverter implements Converter<String,User> {
@Override
public User convert(String source) {//wjj-male-note
User user = new User();
String[] split = source.split("-");
user.setUserName(split[0]);
user.setSex(split[1].equals("male")?SexEnum.MALE:SexEnum.FEMALE);
user.setNote(split[2]);
return user;
}
}
//转换器测试
//http://localhost:8080/string2User/user=wjj-male-note
@RequestMapping("/string2User")
@ResponseBody
public User string2User(User user){
return user;
}
数据验证
基于JSR注解
<!--validation 开启jsr等一系列验证-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
public class ValidatorPojo {
@NotNull(message = "id不能为空")
private Long id;
@Future(message = "需要一个将来日期")
// @Past 只能是过去时间
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd") //日期格式转换,不是验证
private Date date;
@NotNull
@DecimalMin(value = "0.1")
@DecimalMax(value = "10000.00")
private Double doubleValue = null;
@Min(value = 1,message = "最小值为1")
@Max(value = 88,message = "最大值为88")
@NotNull
private Integer integer;
@Range(min = 1,max = 888,message = "范围为1至888")
private Long range;
@Email(message = "邮箱格式错误")
private String email;
@Size(min = 20,max = 30,message = "字符串长度要求20到30间")
private String size;
/*setter getter*/
}
//jsr验证
@RequestMapping("/jsr")
@ResponseBody
public Map<String,Object> jsr(@Valid ValidatorPojo vp, Errors errors){
HashMap<String, Object> map = new HashMap<>();
List<ObjectError> errorList = errors.getAllErrors();
for (ObjectError oe :
errorList) {
String key = null;
String msg = null;
if (oe instanceof FieldError){//字段错误
FieldError fe =(FieldError)oe;
key = fe.getField();
}else {//非字段错误
key = oe.getObjectName();
}
msg = oe.getDefaultMessage();
map.put(key,msg);
}
return map;
}
注意:当需要对方法参数(@RequestParam注解的参数)进行验证时,需要在Controller上添加@Validated注解。
自定义验证机制
public class UserValidator implements Validator {
//用于判断支不支持验证该类
@Override
public boolean supports(Class<?> aClass) {
return aClass.equals(User.class);
}
//验证逻辑
@Override
public void validate(Object o, Errors errors) {
if (o == null){
errors.rejectValue("",null,"用户不能为空");
}
User user = (User)o;
if (!StringUtils.hasText(user.getUserName())){
//(@Nullable String field, String errorCode, String defaultMessage)
errors.rejectValue("userName",null,"用户名不能为空");
}
}
}
@InitBinder
public void initBinder(WebDataBinder binder){
//绑定验证器
binder.setValidator(new UserValidator());
//定义日期参数格式,参数不再需要注解@DateTimeFormat, boolean表示是否允许为空
binder.registerCustomEditor(Date.class,
new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"),false));
}
@RequestMapping("/validator")
@ResponseBody
public Map<String,Object> validator(@Valid User user,
Errors errors,Date date){
HashMap<String, Object> map = new HashMap<>();
map.put("user",user);
map.put("date",date);
if (errors.hasErrors()){
List<ObjectError> errorList = errors.getAllErrors();
for (ObjectError oe:
errorList) {
if (oe instanceof FieldError){
FieldError fe = (FieldError)oe;
map.put(fe.getField(),fe.getDefaultMessage());
}else {
map.put(oe.getObjectName(),oe.getDefaultMessage());
}
}
}
return map;
}
拦截器
public class Interceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");
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("处理器完成方法");
}
}
@SpringBootApplication
//注册拦截器
@Configuration//声明配置类
public class Springboot2Application implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(Springboot2Application.class, args);
}
//实现WebMvcConfigurer接口,注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration ir = registry.addInterceptor(new Interceptor1());
ir.addPathPatterns("/interceptor/*");//指定拦截匹配模式
}
}
当有多个拦截器时,按注册的顺序执行
interceptor1 处理器前
interceptor2 处理器前
interceptor2 处理器后
interceptor1 处理器后
interceptor2 处理器完成方法
interceptor1 处理器完成方法
使用注解获取Session、Cookie、请求头值
@GetMapping("/s1")
public void save(String id, String name,HttpSession session, HttpServletResponse response){
session.setAttribute("id",id);
response.addCookie(new Cookie("name",name));
}
@GetMapping("/s2")
public void get(@SessionAttribute("id")String id,@CookieValue("name")String name){
System.out.println(name+":"+id);
}
请求头可以使用@RequestHeader获取
为控制器添加通知
可以强化控制器,进行参数格式校验和异常处理等操作。下面以异常处理为例
@ControllerAdvice("com.example.springboot2.controller")
public class MyControllerAdvice {
//定义异常发生后的操作,该方法执行后
@ExceptionHandler(NotFoundException.class) //指定要处理的异常,可以设置多个
@ResponseBody //返回json数据
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//返回状态码500
public Map<String,Object> exception(HttpServletRequest request,
NotFoundException ex){
HashMap<String, Object> map = new HashMap<>();
map.put("code",ex.getCode());
map.put("msg",ex.getMsg());
return map;
}
//定义控制器参数绑定规则
@InitBinder
//在控制器方法执行前,对数据模型进行操作
@ModelAttribute
}
静态资源访问
默认是定位到/META-INF/resources/、/resources/、/static/、/public/文件夹下
使用http://localhost:8080/index.html就可以访问到static文件夹下的index.html文件
可以通过修改配置文件和定义配置类来进行资源路径修改
通过配置文件:
# 修改静态资源地址前缀url,必须有mystatic
spring.mvc.static-path-pattern=/mystatic/*
# 静态地址定位
spring.web.resources.static-locations=classpath:static1/
# 使用http://localhost:8080/mystatic/m.txt就能访问到resources/static1/m.txt
通过新增配置类:
public class WebAppConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/mystatic/*").addResourceLocations("classpath:/mystatic/");
}
}
Spring其他技术
异步线程池使用
使用情况:例如A点击训练模型,但不用等到模型训练完毕再返回结果,可以开启另一个线程进行模型的训练
/**
* created by WuJiaJun on 2021/7/24.
* 配置定义线程池和异步启用
*/
@Configuration
@EnableAsync//启用异步
public class AsyncConfig implements AsyncConfigurer {
//定义线程池
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(10);//核心线程数
taskExecutor.setMaxPoolSize(30);//线程池最大线程数
taskExecutor.setQueueCapacity(2000);//线程队列最大线程数
taskExecutor.initialize();
return taskExecutor;
}
}
//AsyncService 单纯接口类无需进行其他配置
@Service
public class AsyncServiceImpl implements AsyncService {
@Override
@Async//声明使用异步调用
public void generateReport() {
//打印异步线程名称
System.out.printf("报表线程名称:[%s]\n",Thread.currentThread().getName());
}
}
//根据控制台输出可以看见,Service是启用了另一个线程来运行
@Controller
public class AsyncController {
@Autowired
private AsyncService asyncService;
@GetMapping("/page")
public String asyncPage(){
System.out.printf("请求线程名称:[%s]\n",Thread.currentThread().getName());
//调用异步服务
asyncService.generateReport();
return "async";
}
}
异步消息队列
有点对点和发布订阅两种形式。
点对点就是将一个系统的消息发布到指定的另外一个系统。
发布订阅模式是一个系统约定将消息发布到一个主题中,然后各个系统就你那通过订阅这个主题来接受消息。
这里讲的是发布订阅模式。
ActiveMq
<!--异步消息:ActiveMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.messaginghub</groupId>
<artifactId>pooled-jms</artifactId>
<version>1.0.4</version>
</dependency>
# ActiveMQ
# url地址
spring.activemq.broker-url=tcp://localhost:61616
# 配置用户名密码
spring.activemq.user=admin
spring.activemq.password=admin
# 是否使用发布订阅模式,默认为false,即用的是点对点的模式
spring.jms.pub-sub-domain=true
# 默认目的地址
spring.jms.template.default-destination=activemq.default.destination
# 是否启用连接池
spring.activemq.pool.enabled=true
# 连接池最大连接数配置
spring.activemq.pool.max-connections=50
# 配置ActiveMq信任User类
# spring.activemq.packages.trusted=com.example.springboot2.pojo,java.lang,com.example.springboot2.enums
spring.activemq.packages.trust-all=true
@Service
public class ActiveMqUserServiceImpl implements ActiveMqUserService {
@Autowired
private JmsTemplate jmsTemplate;
private static final String destination = "d1";
@Override
public void sendUser(User user) {
System.out.printf("发送消息[%s]\n",user);
jmsTemplate.convertAndSend(destination,user);
}
@Override
@JmsListener(destination = destination)//定义监听地址
public void receiveUser(User user) {
System.out.printf("接收到消息[%s]\n",user);
}
}
//必须在配置文件中信任该类
//该类必须实现序列化接口
public class User implements Serializable {
private static final long serialVersionUID = 8081515489153541L;
private Long id;
private String userName;
private SexEnum sex;
private String note;
/*getter setter construct*/
}
消息发送后,监听的Service会自动执行方法
//测试普通消息发送
@GetMapping("/msg")
public void msg(){
String msg = "abc";
activeMqService.sendMsg(msg);
}
//测试User对象的发送
@GetMapping("/user")
public void sendUser(){
User user = new User(123L, "wjj", SexEnum.MALE, "note");
activeMqUserService.sendUser(user);
}
RabbitMq
<!--异步消息:amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
# RabbitMQ
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
# 消息队列名称,发送给字符串(自定义配置)
rabbitmq.queue.msg=spring-boot-queue-msg
# 消息队列名称,发送用户对象(自定义配置)
rabbitmq.queue.user=spring-boot-queue-user
启动类里配置消息队列
//创建RabbitMQ队列
@Value("${rabbitmq.queue.msg}")
private String msgQueueName;
@Value("rabbitmq.queue.user")
private String userQueueName;
@Bean
public Queue createQueueMsg(){
//boolean表示是否持久化消息
return new Queue(msgQueueName,true);
}
@Bean
public Queue createQueueUser(){
//boolean表示是否持久化消息
return new Queue(userQueueName,true);
}
@Service
public class RabbitMqServiceImpl implements RabbitMqService,RabbitTemplate.ConfirmCallback {
@Value("${rabbitmq.queue.msg}")
private String msgRouting;
@Value("rabbitmq.queue.user")
private String userRouting;
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void sendMsg(String msg) {
System.out.printf("发送消息:%s\n",msg);
//设置回调
rabbitTemplate.setConfirmCallback(this);
//发送消息
rabbitTemplate.convertAndSend(msgRouting,msg);
}
@Override
public void sendUser(User user) {
System.out.printf("发送消息:%s\n",user);
//设置回调
rabbitTemplate.setConfirmCallback(this);
//发送消息
rabbitTemplate.convertAndSend(userRouting,user);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
if (b){
System.out.println("消息消费成功");
}else {
System.out.println("信息消费失败");
}
}
}
@Component
public class RabbitMessageReceiver {
//定义监听字符串队列名称
@RabbitListener(queues = {"${rabbitmq.queue.msg}"})
public void receiveMsg(String msg){
System.out.println("收到消息:"+msg);
}
//定义监听用户队列名称
@RabbitListener(queues = {"${rabbitmq.queue.msg}"})
public void receiveUser(String msg){
System.out.println("收到消息:"+msg);
}
}
@Controller
@RequestMapping("/rb")
public class RabbitMqController {
@Autowired
private RabbitMqService rabbitMqService;
@GetMapping("/msg")
public void msg(){
rabbitMqService.sendMsg("abc");
}
@GetMapping("/user")
public void user(){
User user = new User(123L, "wjj", SexEnum.MALE, "note");
rabbitMqService.sendUser(user);
}
}
注:windows安装rabbitMq太麻烦,就没测试
定时任务
@Scheduled更多详细配置请看书
//启动类加上注解
@EnableScheduling//允许定时任务执行
@Service
public class ScheduleServiceImpl implements ScheduleService {
int count1 = 1;
int count2 = 1;
//单位ms,每隔1s执行一次,启动类启动后就开始执行该方法
@Scheduled(fixedRate = 1000)
@Async//异步执行
@Override
public void job1() {
System.out.printf("job1 [%s],执行第[%d]次\n",
Thread.currentThread().getName(),
count1++);
}
@Scheduled(fixedRate = 1000)
@Async//异步执行
@Override
public void job2() {
System.out.printf("job2 [%s],执行第[%d]次\n",
Thread.currentThread().getName(),
count2++);
}
}
WebSocket和客户端通信
webSocket允许服务端主动与客户端通信。
<!--websocket-->
<!--加入security是为了保证安全性-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
@ServerEndpoint("/ws")
@Service
public class WebSocketServiceImpl {
//记录在线连接数,应该设计成线程安全的
private static int onlineCount=0;
//concurrent包的线程安全set,用来存放每个客户端对应的WebSocketServiceImpl
private static CopyOnWriteArraySet<WebSocketServiceImpl>
webSocketSet = new CopyOnWriteArraySet<>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//连接建立成功调用的方法
@OnOpen
public void onOpen(Session session){//必须配置spring security的情况下session才不会为空,否则要自定义session
this.session = session;
webSocketSet.add(this);//加入set中
addOnlineCount();
System.out.println("有新连接加入,当前在线人数为:"+getOnlineCount());
try {
sendMsg("有新连接加入!");
} catch (IOException e) {
e.printStackTrace();
}
}
//连接关闭调用的方法
@OnClose
public void onClose(){
webSocketSet.remove(this);
subOnlineCount();
System.out.println("有连接关闭,当前在线人数为:"+getOnlineCount());
}
//收到客户端消息后调用的方法
@OnMessage
public void onMessage(String message,Session session){
System.out.println("来自客户端的消息:"+message);
//群发消息
for (WebSocketServiceImpl item :
webSocketSet) {
String name = item.getSession().getUserPrincipal().getName();
System.out.println(name);
try {
item.sendMsg(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
//发生错误时调用
@OnError
public void onError(Session session, Throwable error){
System.out.println("发生错误");
error.printStackTrace();
}
private void sendMsg(String msg) throws IOException {
this.session.getBasicRemote().sendText(msg);
}
private static synchronized int getOnlineCount(){
return onlineCount;
}
private static synchronized void addOnlineCount(){
WebSocketServiceImpl.onlineCount++;
}
private static synchronized void subOnlineCount(){
WebSocketServiceImpl.onlineCount--;
}
public Session getSession() {
return session;
}
}
@RequestMapping("/websocket")
@Controller
public class WebSocketController {
@GetMapping("/index")
public String websocket(){
return "websocket";
}
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
<script type="text/javascript" src="./../static/jquery-3.3.1.min.js"></script>
</head>
<body>
测试WebSocket站点
<input id="message" type="text">
<button onclick="sendMessage()">发送消息</button>
<button onclick="closeWebsocket()">关闭websocket连接</button>
<div id="context"></div>
<script>
var websocket = null;
//判断当前浏览器是否支持websocket
if ('WebSocket' in window){
websocket = new WebSocket("ws://localhost:8080/ws");
} else {
alert('Not Support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function () {
appendMessage("error")
}
//连接成功建立的回调方法
websocket.onopen = function () {
appendMessage("open")
}
//接收到消息回调
websocket.onmessage = function (ev) {
appendMessage(ev.data)
}
//连接关闭的回调
websocket.onclose = function () {
appendMessage("close")
}
//监听窗口关闭,窗口关闭时,主动关闭websocket连接
window.onbeforeunload = function (ev) {
websocket.close()
}
//将消息显示在网页上
function appendMessage(message) {
var context = $("#context").html() + "<br/>" +message;
$("#context").html(context)
}
//关闭连接
function closeWebsocket() {
websocket.close();
}
//发送消息
function sendMessage() {
var message = $("#message").val();
websocket.send(message);
}
</script>
</body>
</html>
stomp和客户端通信
并不是所有浏览器都支持websocket,这时可以使用stomp,页面构建很麻烦,具体请看书。
整合MongoDB
MongDB简介
MongoDB是一个文档类型数据库,和redis一样是nosql的。
MongoDB没有表的概念,每一个数据库下一级存储单位是“集合”,集合存储了一系列文档数据。
单个文档示例如下(就是json化的对象):
{
"_id" : NumberLong(10),
"sex" : "MALE",
"user_name" : "wjj",
"note" : "n",
"_class" : "com.example.springwebflux2.pojo.User"
}
MongoDB操作
mongod -db:path D:\MongoDB\Server\5.0\data\db #开启服务
mongo #连接数据库
show databases #查看目前所有的数据库
use test #使用test数据库,没有则会创建该数据库
db.createCollection("runoob") #创建名为“runoob”的集合
show tables # 查看当前数据库集合
show collections # 查看当前数据库集合
db.collection.insert(document) #往collection集合中插入document
db.collection.find().pretty() #查看collection集合的所有文档
Spring+JPA+MongDB
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
spring:
data:
mongodb:
port: 27017
host: localhost
database: mongodbtest
@Data//lombok
public class User {
private Integer id;
private String name;
}
public interface UserDao extends MongoRepository<User,Integer> {
}
@Service
public class UserService {
@Autowired
UserDao userDao;
public void save(User user){
userDao.save(user);
}
public List<User> findAll(){
return userDao.findAll();
}
}
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/save")
public String save(User user){
userService.save(user);
return "保存成功";
}
@RequestMapping("/find")
public List<User> find(){
return userService.findAll();
}
}
启动类无需额外配置。使用save方法后,会创建mongodbtest数据库,并在其中创建user集合,然后再插入文档。
show databases
admin 0.000GB
config 0.000GB
local 0.000GB
mongodbtest 0.000GB
springboot 0.000GB
use mongodbtest
switched to db mongodbtest
show tables
user
db.user.find().pretty()
{ “_id” : 1, “name” : “wjj”, “_class” : “com.example.mongdbtest.vo.User” }
高并发处理
业务说明:模拟高并发抢购商品。这里只有商品表和交易记录表。
//商品类
@Data
@Alias("product")
public class ProductPo implements Serializable {
private Long id;
private String productName;
private int stock;
private double price;
private int version;
private String note;
}
//交易记录类
@Data
@Alias("purchaseRecord")
public class PurchaseRecordPo {
private Long id;
private Long userId;
private Long productId;
private double price; //单价
private int quantity; //商品数量
private double sum; //总价
private Timestamp purchaseTime;
private String note;
}
@Repository
public interface ProductDao {
ProductPo getProduct(Long id);
int decreaseProduct(@Param("id") Long id,
@Param("quantity") int quantity;
}
@Repository
public interface PurchaseRecordDao {
int insertPurchaseRecord(PurchaseRecordPo pr);
}
正常的购买流程如下
Service
public class PurchaseServiceImpl implements PurchaseService {
@Autowired
private ProductDao productDao;
@Autowired
private PurchaseRecordDao purchaseRecordDao;
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
public boolean purchase(Long userId, Long productId, int quantity) {
ProductPo product = productDao.getProduct(productId);
if (product.getStock()<quantity){
return false;//库存不足
}
//扣库存
int result = productDao.decreaseProduct(productId,quantity);
if (result == 0){
return false;
}
//生成交易记录
PurchaseRecordPo pr = this.generatePurchaseRecord(userId,product,quantity);
//插入交易记录
purchaseRecordDao.insertPurchaseRecord(pr);
return true;
}
private PurchaseRecordPo generatePurchaseRecord(Long userId, ProductPo product, int quantity){
PurchaseRecordPo record = new PurchaseRecordPo();
record.setNote("购买日志,时间:"+System.currentTimeMillis());
record.setPrice(product.getPrice());
record.setProductId(product.getId());
record.setQuantity(quantity);
double sum = product.getPrice() * quantity;
record.setSum(sum);
record.setUserId(userId);
return record;
}
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>购买产品测试</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
</head>
<script>
//30000库存,50000次购买请求
for (var i = 0; i < 50000; i++) {//异步请求
var params = {
userId:1,
productId:1,
quantity:1
};
$.post("./purchase",params,function (result) {
//alert(result.message)
})
}
</script>
<body>
<h1>抢购商品测试</h1>
</body>
</html>
会出现超发的情况,查看交易记录的数量,会发现不止30000条,同时商品的数量也变为负数。
悲观锁
在获取商品信息sql后加上for update
在执行这条sql的事务中会锁定商品信息,不让其他线程进行访问,可以解决超发的问题
缺点是会造成线程堵塞,大幅度增加业务完成时间。
悲观锁是利用数据库自带的机制。
<select id="getProduct" parameterType="long" resultType="product">
select id,product_name as productName,
stock,price,version,note from t_product
where id = #{id} for update
</select>
乐观锁
对商品信息设定版本号,每次进行修改时,version+1。
取出商品信息时获得version值,当扣商品库存时,校验version是否发生改变。
如果改变了则取消本次业务,没有则继续进行。
@Repository
public interface ProductDao {
int decreaseProduct(@Param("id") Long id,
@Param("quantity") int quantity,
@Param("version") int version);
}
<update id="decreaseProduct" parameterType="map">
update t_product set stock = stock-#{quantity},
version = version + 1
where id = #{id} and version = #{version}
</update>
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
public boolean purchase(Long userId, Long productId, int quantity) {
ProductPo product = productDao.getProduct(productId);
if (product.getStock()<quantity){
return false;//库存不足
}
//扣库存
int version = product.getVersion();
int result = productDao.decreaseProduct(productId,quantity,version);
if (result == 0){
return false;
}
//生成交易记录
PurchaseRecordPo pr = this.generatePurchaseRecord(userId,product,quantity);
//插入交易记录
purchaseRecordDao.insertPurchaseRecord(pr);
return true;
}
可以解决超发问题,并且相较于悲观锁速度有了很大提升,但仍然存在问题:
查看库存值,发现还有剩余,这是因为在高并发状态下,很多业务因为version不符而取消。
缓解办法:1、在一定时间内(如100ms)重试该方法。2、指定重试次数,如执行三次。
这两种办法都只能缓解而非解决问题。
方法1的弊端为,如果请求量较大,后台负载较大会导致指定时间内的重试次数变少。
//method 1
long start = System.currentTimeMillis();
while (true){
long end = System.currentTimeMillis();
if (end -start>100){
return false;
}
//如果发生错误:version不符则重试
}
//method2
for(int i=0;i<3;i++){
//执行业务,成功则跳出循环
}
SpringBoot读取配置文件
基于注解
(1)@Value(常用)。读取简单的一项属性。只有放在Bean中才有效。因为Bean才会被Spring容器管理。
@Value("${server.port}")
private Integer port;
对应配置:
server:
port: 8080
(2)@ConfigurationProperties(常用)。读取一系列的配置信息。需要被配置成Bean。
其原理应该是通过set方法进行属性注入。
@Component
@ConfigurationProperties(prefix = "key.bug")
@Setter
@Getter
public class BugProperty {
private String level;
private String handler;
private Integer maxValue;
}
对应配置:
key:
bug:
level: dubug
handler: manager
maxValue: 10
基于编程
通过Environment类进行配置的读取
@Autowired
private Environment environment;
// 代码中
environment.getProperty("seafile.admin.user")
个人感觉还是基于注解的方式更好