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

  1. 配置扫描
@SpringBootApplication
@ComponentScan(basePackages = {"com.example.springboot2"}) //有了上面的注解,可以不用加
public class Springboot2Application {

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

}
  1. 以注解方式配置bean
public interface IndexService {
    void doService();
}
@Service
public class IndexServiceImpl implements IndexService {
    @Override
    public void doService() {
        System.out.println("doService");
    }
}
  1. 注入bean并使用
@RequestMapping("/testIOC")
public String testIOC(){
    indexService.doService();
    return "index";
}
书中讲了很多使用配置类来代替配置文件。

使用配置文件

可以在默认的application.properties中进行自定义配置,并且读取
  1. 添加自定义配置
# 自定义属性配置
database.driverName=com.jdbc.Driver
database.url = jdbc:mysql://localhost:3306/springboot
database.username = root
database.password = 123456
  1. 使用@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。
  1. 引入数据源和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
  1. 编写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);
}
  1. 注解配置
//启动类注解
@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缓存

  1. 为启动类添加@EnableCaching//开起缓存机制
  2. 配置文件
# 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
  1. 使用注解开启缓存
    @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")

个人感觉还是基于注解的方式更好

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值