2022年4月

申请账号

点击扫码申请Server Turbo

申请成功后在https://sct.ftqq.com/sendkey可查看你自己的key,后面用于配置

代码示例

服务调用

package top.roothk.mall.service.impl;

import jodd.http.HttpRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import top.roothk.mall.service.MessageService;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
 * @author hongkeng
 */
@Slf4j
@Service
public class MessageServiceImpl implements MessageService {

    @Value("${system.message.exception.server-turbo.key}")
    private String serverTurboKey;
    @Value("${spring.profiles.active:default}")
    private String activeProfile;

    private static final String SERVER_TURBO_URL = "https://sctapi.ftqq.com/%s.send";

    @Async
    @Override
    public void exception(String message) {
        if (!"prod".equals(activeProfile)) {
            return;
        }
        sendServerTurbo(serverTurboKey, "异常信息", message);
    }

    public static void sendServerTurbo(String serverTurboKey, String title, String message) {
        HttpRequest.get(String.format(SERVER_TURBO_URL, serverTurboKey))
                .connectionTimeout(60000)
                .timeout(60000)
                .charset("utf-8")
                .query("title", title)
                .query("desp", message)
                .send();
    }

}

全局异常处理

package top.roothk.mall.exception;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import top.roothk.mall.ResultBean;
import top.roothk.mall.service.MessageService;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 服务器异常
 *
 * @author porridge
 */
@Slf4j
@RestControllerAdvice
public class RestExceptionController {

    @Resource
    private MessageService messageService;

    /**
     * 自定义异常类
     */
    @ExceptionHandler(value = ServiceException.class)
    public ResultBean<String> commonServiceException(HttpServletRequest req, HttpServletResponse resp, ServiceException e) {
        ResultBean<String> result = new ResultBean<>();
        result.setStatus(e.getRetCode());
        result.setMessage(e.getMessage());
        log.error("业务错误, {}", e.getMessage());
        messageService.exception(e.getMessage());
        return result;
    }

    /**
     * 未找到该方法或方法类型错误
     */
    @ExceptionHandler(value = {HttpRequestMethodNotSupportedException.class})
    public ResultBean<String> notFindException(HttpServletRequest req, HttpServletResponse resp, HttpRequestMethodNotSupportedException e) {
        resp.setStatus(404);
        log.error("未找到该地址: {}", req.getServletPath(), e);
        messageService.exception(e.getMessage());
        return new ResultBean<>(404, "未找到该地址");
    }

    @ExceptionHandler(value = {IllegalArgumentException.class})
    public ResultBean<String> illegalArgumentException(HttpServletRequest req, HttpServletResponse resp, IllegalArgumentException e) {
        log.error("IllegalArgumentException", e);
        messageService.exception(e.getMessage());
        return new ResultBean<>(400, e.getMessage());
    }

    /**
     * 参数验证失败 (注意:返回的 HTTP Status 是 400)
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = {
            MethodArgumentNotValidException.class,
            MethodArgumentTypeMismatchException.class,
            MissingServletRequestParameterException.class,
            TypeMismatchException.class})
    public ResultBean<List<String>> validationException(HttpServletRequest req, HttpServletResponse resp, Exception e) {
        ResultBean<List<String>> result = new ResultBean<>();
        String msg = "参数验证失败";

        if (e instanceof MethodArgumentNotValidException exception) {
            List<String> errors = exception.getBindingResult()
                    .getFieldErrors()
                    .stream()
                    .map(x -> x.getField() + " 验证失败:" + x.getRejectedValue())
                    .collect(Collectors.toList());
            msg = JSON.toJSONString(errors);
        }

        if (e instanceof MethodArgumentTypeMismatchException exception) {
            msg = exception.getMessage();
        }
        log.info("参数验证失败 报警{} {} {}", "请求参数转换失败", JSON.toJSONString(result), e.getClass().toString());
        log.error("error", e);
        messageService.exception(e.getMessage());

        result.setStatus(400);
        result.setMessage(msg);
        result.setData(new ArrayList<>());
        resp.setStatus(400);
        return result;
    }

    @ExceptionHandler(value = {BindException.class})
    public ResultBean<List<String>> validationBindException(HttpServletRequest req, HttpServletResponse resp, BindException e) {
        resp.setStatus(400);
        ResultBean<List<String>> result = new ResultBean<>();
        result.setStatus(400);
        result.setMessage("类型验证错误");
        List<ObjectError> objectErrors = e.getAllErrors();
        List<String> errors = new ArrayList<>();
        for (ObjectError error : objectErrors) {
            errors.add(Objects.requireNonNull(error.getCodes())[1] + error.getDefaultMessage());
        }
        result.setData(errors);
        log.error("类型验证错误, {}", e.getMessage(), e);
        messageService.exception(e.getMessage());
        return result;
    }

    /**
     * 服务器通用异常
     */
    @ExceptionHandler(value = Exception.class)
    public ResultBean<String> handleException(HttpServletRequest req, HttpServletResponse resp, Exception e) {
        resp.setStatus(500);
        log.error("服务器异常: {}", e.getMessage(), e);
        messageService.exception(e.getMessage());
        return new ResultBean<>(500, "服务器异常: {}", e.getMessage());
    }

    /**
     * 获取错误信息完整信息
     *
     * @param e 错误
     * @return 错误信息
     */
    private String getErrorMessage(Exception e) {
        ByteArrayOutputStream b = new ByteArrayOutputStream();
        e.printStackTrace(new PrintStream(b));
        return b.toString();
    }

}

# 依赖版本
OpenJDK 17
Spring Boot 2.6.6
MySQL 8
QueryDSL 5.0

Maven POM

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>com.mysema.maven</groupId>
            <artifactId>apt-maven-plugin</artifactId>
            <version>1.1.3</version>
            <executions>
                <execution>
                    <goals>
                        <goal>process</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/generated-sources/java</outputDirectory>
                        <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Config

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;

@Configuration
public class JpaConfig {

    @Bean
    public JPAQueryFactory jpaQuery(EntityManager entityManager) {
        return new JPAQueryFactory(entityManager);
    }

}

BaseService.java

package top.roothk.mall.service;

import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.QBean;
import com.querydsl.core.types.dsl.EntityPathBase;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import top.roothk.mall.PageRequest;
import top.roothk.mall.entity.BaseEntity;

import java.io.Serializable;
import java.util.List;

/**
 * Service - 基类
 *
 * @param <T>  实体
 * @param <ID> id
 * @author RootHK
 */
public interface BaseService<T extends BaseEntity<ID>, ID extends Serializable> {

    EntityPathBase<T> getQueryDslEntity();

    /**
     * 查找实体对象
     *
     * @param id ID
     * @return 实体对象,若不存在则返回null
     */
    T find(ID id);

    T find(Predicate p);

    T find(List<Predicate> p);

    /**
     * 查找实体对象集合
     *
     * @param ids ID
     * @return 实体对象集合
     */
    List<T> findList(List<ID> ids);

    List<T> findList(Predicate p);

    List<T> findList(Predicate p, OrderSpecifier s);

    List<T> findList(List<Predicate> p, List<OrderSpecifier> s);

    /**
     * 查找实体对象集合
     */
    List<T> findList(Sort sort);

    /**
     * 查找所有实体对象集合
     *
     * @return 所有实体对象集合
     */
    List<T> findList();

    /**
     * 查找实体对象分页
     *
     * @param pageable 分页信息
     * @return 实体对象分页
     */
    Page<T> page(Pageable pageable);

    /**
     * 查找实体对象分页
     *
     * @param r 分页信息
     * @return 实体对象分页
     */
    top.roothk.mall.Page<T> page(PageRequest r);

    <Y> top.roothk.mall.Page<Y> page(QBean<Y> select, PageRequest r, Predicate p, OrderSpecifier s);

    top.roothk.mall.Page<T> page(PageRequest r, Predicate p, OrderSpecifier s);

    top.roothk.mall.Page<T> page(PageRequest r, List<Predicate> p, List<OrderSpecifier> s);

    /**
     * 查询实体对象总数
     *
     * @return 实体对象总数
     */
    long count();

    /**
     * 判断实体对象是否存在
     *
     * @param id ID
     * @return 实体对象是否存在
     */
    boolean exists(ID id);

    /**
     * 判断实体对象是否存在
     *
     * @param p 条件
     * @return 实体对象是否存在
     */
    boolean exists(Predicate p);

    /**
     * 保存实体对象
     *
     * @param entity 实体对象
     * @return 实体对象
     */
    T save(T entity);

    /**
     * 删除实体对象
     *
     * @param id ID
     */
    void delete(ID id);

    /**
     * 删除实体对象
     *
     * @param ids ID
     */
    void delete(List<ID> ids);

    /**
     * 删除实体对象
     *
     * @param entity 实体对象
     */
    void delete(T entity);

    /**
     * 删除实体对象
     *
     * @param entities 实体对象
     */
    void deleteEntity(List<T> entities);

    /**
     * 落库
     */
    void flush();

}

BaseServiceImpl.java

package top.roothk.mall.service.impl;

import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.QBean;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import top.roothk.mall.PageRequest;
import top.roothk.mall.entity.BaseEntity;
import top.roothk.mall.service.BaseService;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;

/**
 * Service - 基类
 *
 * @author RootHK
 * @version 5.0
 */
@Slf4j
@Transactional(rollbackFor = Exception.class)
public abstract class BaseServiceImpl<T extends BaseEntity<ID>, ID extends Serializable> implements BaseService<T, ID> {

    @Resource
    protected JPAQueryFactory queryFactory;

    @Autowired
    protected JpaRepository<T, ID> repository;

    @Override
    public T find(ID id) {
        return repository.findById(id).orElse(null);
    }

    @Override
    public T find(Predicate p) {
        return find(p == null ? null : Collections.singletonList(p));
    }

    @Override
    public T find(List<Predicate> p) {
        JPAQuery<T> q = queryFactory.selectFrom(getQueryDslEntity());
        if (p != null && p.size() > 0) {
            q.where(p.toArray(new Predicate[0]));
        }
        return q.fetchFirst();
    }

    @Override
    public List<T> findList(List<ID> ids) {
        return repository.findAllById(ids);
    }

    @Override
    public List<T> findList(Predicate p) {
        return findList(p, null);
    }

    @Override
    public List<T> findList(Predicate p, OrderSpecifier s) {
        return findList(p == null ? null : Collections.singletonList(p),
                s == null ? null : Collections.singletonList(s));
    }

    @Override
    public List<T> findList(List<Predicate> p, List<OrderSpecifier> s) {
        JPAQuery<T> q = queryFactory.selectFrom(getQueryDslEntity());
        if (p != null && p.size() > 0) {
            q.where(p.toArray(new Predicate[0]));
        }
        if (s != null && s.size() > 0) {
            q.orderBy(s.toArray(new OrderSpecifier[0]));
        }
        return q.fetch();
    }

    @Override
    public List<T> findList(Sort sort) {
        return repository.findAll(sort);
    }

    @Override
    public List<T> findList() {
        return repository.findAll();
    }

    @Override
    public Page<T> page(Pageable pageable) {
        return repository.findAll(pageable);
    }

    @Override
    public top.roothk.mall.Page<T> page(PageRequest r) {
        return new top.roothk.mall.Page<>(page(r.pageRequest()));
    }

    @Override
    public <Y> top.roothk.mall.Page<Y> page(QBean<Y> select, PageRequest r, Predicate p, OrderSpecifier s) {
        Integer page = r.getPage();
        Integer size = r.getSize();
        JPAQuery<Y> q = queryFactory.select(select).from(getQueryDslEntity());
        JPAQuery<Long> qc = queryFactory.select(getQueryDslEntity().count());
        if (p != null) {
            q.where(p);
            qc.where(p);
        }
        List<Long> c = qc.from(getQueryDslEntity()).fetch();
        Long allCount = c.get(0);
        if (allCount <= 0) {
            return top.roothk.mall.Page.empty(r);
        }
        if (s != null) {
            q.orderBy(s);
        }
        List<Y> l;
        //分页查询
        if (page != null && size != null) {
            l = q.offset((long) (page) * size)
                    .limit(size)
                    .fetch();
        } else {
            l = q.fetch();
        }
        return new top.roothk.mall.Page<>(r, allCount, l);
    }

    @Override
    public top.roothk.mall.Page<T> page(PageRequest r, Predicate p, OrderSpecifier s) {
        return page(r,
                p == null ? null : Collections.singletonList(p),
                s == null ? null : Collections.singletonList(s));
    }

    @Override
    public top.roothk.mall.Page<T> page(PageRequest r, List<Predicate> p, List<OrderSpecifier> s) {
        Integer page = r.getPage();
        Integer size = r.getSize();
        JPAQuery<T> q = queryFactory.selectFrom(getQueryDslEntity());
        JPAQuery<Long> qc = queryFactory.select(getQueryDslEntity().count());
        if (p != null && p.size() > 0) {
            q.where(p.toArray(new Predicate[0]));
            qc.where(p.toArray(new Predicate[0]));
        }
        List<Long> c = qc.from(getQueryDslEntity()).fetch();
        Long allCount = c.get(0);
        if (allCount <= 0) {
            return top.roothk.mall.Page.empty(r);
        }
        if (s != null && s.size() > 0) {
            q.orderBy(s.toArray(new OrderSpecifier[0]));
        }
        List<T> l;
        //分页查询
        if (page != null && size != null) {
            l = q.offset((long) (page) * size)
                    .limit(size)
                    .fetch();
        } else {
            l = q.fetch();
        }
        return new top.roothk.mall.Page<>(r, allCount, l);
    }

    @Override
    public long count() {
        return repository.count();
    }

    @Override
    public boolean exists(ID id) {
        return repository.existsById(id);
    }

    @Override
    public boolean exists(Predicate p) {
        Integer exists = queryFactory.selectOne().from(getQueryDslEntity()).where(p).fetchFirst();
        return exists != null && exists > 0;
    }

    @Override
    public T save(T entity) {
        if (entity.getId() != null) {
            T t = find(entity.getId());
            BeanUtils.copyProperties(entity, t);
            return repository.save(t);
        }
        return repository.save(entity);
    }

    @Override
    public void delete(ID id) {
        repository.deleteById(id);
    }

    @Override
    public void delete(List<ID> ids) {
        repository.deleteAllById(ids);
    }

    @Override
    public void deleteEntity(List<T> entities) {
        Assert.notNull(entities, "删除对象为空");
        repository.deleteAllInBatch(entities);
    }

    @Override
    public void delete(T entity) {
        repository.delete(entity);
    }

    @Override
    public void flush() {
        repository.flush();
    }

}

AccountService.class

package top.roothk.mall.service;

import top.roothk.mall.entity.Account;

public interface AccountService extends BaseService<Account, Long> {}

AccountServiceImpl.class

package top.roothk.mall.service.impl;

import com.querydsl.core.types.dsl.EntityPathBase;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.roothk.mall.entity.Account;
import top.roothk.mall.repository.AccountRepository;
import top.roothk.mall.service.AccountService;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class AccountServiceImpl extends BaseServiceImpl<Account, Long> implements AccountService {

    @Override
    public EntityPathBase<Account> getQueryDslEntity() {
        return Q;
    }

}

使用

// 通过id查询
public String get(Long id) {
    return find(Q.accountId.eq(id)).getName();
}

基于CentOS 7, Jdk 11

server模式压测 需要用到至少三台linux机器
服务器A:部署了被压测程序(jmeter配置界面设置的server host)
服务器B:jmeter-server(实际发起请求的slave端)
服务器C:jmeter(用于向各个jmeter-server发起调用命令的master端)

在B和C服务器上下载Jmeter

下载

最新Jmeter / 5.4.3版本

$ wget https://dlcdn.apache.org//jmeter/binaries/apache-jmeter-5.4.3.tgz

解压

$ tar -zxvf apache-jmeter-5.4.3.tgz

服务器B 配置Jmeter

$ cd apache-jmeter-5.4.3
$ vim bin/jmeter.properties

# 关闭ssl连接
server.rmi.ssl.disable=true

服务器C 配置Jmeter

$ cd apache-jmeter-5.4.3
$ vim bin/jmeter.properties

# 关闭ssl连接
server.rmi.ssl.disable=true
# 服务器B的地址,多个以,分割 如果在同一个服务器上就加上设置的端口port(IP:PORT)
remote_hosts=172.1.0.1,172.1.0.2,172.1.0.3,172.1.0.4

启动服务器B(可多个服务器)

# 执行后就可以挂着等待命令了
$ sh ./bin/jmeter-server

启动服务器C

将GUI端配置好的demo.jmx保存到服务器C的apache-jmeter-5.4.3目录下

初始化

$ mkdir report
$ vim shell.sh

输入以下内容 :wq保存

#/bin/sh
k=$1
# -r 表示远程服务器发起调用,想使用本机调用时去掉-r即可
./bin/jmeter -n -t ./demo.jmx -r -l ./report/$k.csv -e -o ./report/$k

运行

$ ./shell.sh t1

等待执行完成,在report目录下就会生成一个名字为t1的文件夹,下载下来打开index.html就可以看到报告内容了