首页
视频
留言
壁纸
直播
下载
友链
统计
推荐
vue
在线工具
Search
1
ElasticSearch ES 安装 Kibana安装 设置密码
421 阅读
2
记一个报错GC overhead limit exceeded解决方法
344 阅读
3
Teamcity + Rancher + 阿里云Code 实现Devops 自动化部署
230 阅读
4
JAVA秒杀系统的简单实现(Redis+RabbitMQ)
209 阅读
5
分布式锁Redisson,完美解决高并发问题
206 阅读
JAVA开发
前端相关
Linux相关
电商开发
经验分享
电子书籍
个人随笔
行业资讯
其他
登录
/
注册
Search
标签搜索
AOP
支付
小说
docker
SpringBoot
XML
秒杀
K8S
RabbitMQ
工具类
Shiro
多线程
分布式锁
Redisson
接口防刷
Jenkins
Lewis
累计撰写
146
篇文章
累计收到
14
条评论
首页
栏目
JAVA开发
前端相关
Linux相关
电商开发
经验分享
电子书籍
个人随笔
行业资讯
其他
页面
视频
留言
壁纸
直播
下载
友链
统计
推荐
vue
在线工具
搜索到
102
篇与
的结果
2023-01-17
Spring中11个最常用的扩展点
前言在使用spring的过程中,我们有没有发现它的扩展能力很强呢? 由于这个优势的存在,使得spring具有很强的包容性,所以很多第三方应用或者框架可以很容易的投入到spring的怀抱中。今天我们主要来学习Spring中很常用的11个扩展点,你用过几个呢?1. 类型转换器如果接口中接收参数的实体对象中,有一个字段类型为Date,但实际传递的参数是字符串类型:2022-12-15 10:20:15,该如何处理?Spring提供了一个扩展点,类型转换器Type Converter,具体分为3类:Converter<S,T>: 将类型 S 的对象转换为类型 T 的对象ConverterFactory<S, R>: 将 S 类型对象转换为 R 类型或其子类对象GenericConverter:它支持多种源和目标类型的转换,还提供了源和目标类型的上下文。 此上下文允许您根据注释或属性信息执行类型转换。还是不明白的话,我们举个例子吧。定义一个用户对象@Data public class User { private Long id; private String name; private Date registerDate; }实现Converter接口public class DateConverter implements Converter<String, Date> { private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Override public Date convert(String source) { if (source != null && !"".equals(source)) { try { simpleDateFormat.parse(source); } catch (ParseException e) { e.printStackTrace(); } } return null; } }将新定义的类型转换器注入到Spring容器中@Configuration public class WebConfig extends WebMvcConfigurerAdapter { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new DateConverter()); } }调用接口测试@RequestMapping("/user") @RestController public class UserController { @RequestMapping("/save") public String save(@RequestBody User user) { return "success"; } }请求接口时,前端传入的日期字符串,会自动转换成Date类型。2. 获取容器Bean在我们日常开发中,经常需要从Spring容器中获取bean,但是你知道如何获取Spring容器对象吗?2.1 BeanFactoryAware@Service public class PersonService implements BeanFactoryAware { private BeanFactory beanFactory; @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } public void add() { Person person = (Person) beanFactory.getBean("person"); } }实现BeanFactoryAware接口,然后重写setBeanFactory方法,可以从方法中获取spring容器对象。2.2 ApplicationContextAware@Service public class PersonService2 implements ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public void add() { Person person = (Person) applicationContext.getBean("person"); } }实现ApplicationContextAware接口,然后重写setApplicationContext方法,也可以通过该方法获取spring容器对象。2.3 ApplicationListener@Service public class PersonService3 implements ApplicationListener<ContextRefreshedEvent> { private ApplicationContext applicationContext; @Override public void onApplicationEvent(ContextRefreshedEvent event) { applicationContext = event.getApplicationContext(); } public void add() { Person person = (Person) applicationContext.getBean("person"); } }3. 全局异常处理以往我们在开发界面的时候,如果出现异常,要给用户更友好的提示,例如:@RequestMapping("/test") @RestController public class TestController { @GetMapping("/add") public String add() { int a = 10 / 0; return "su"; } }如果不对请求添加接口结果做任何处理,会直接报错:用户可以直接看到错误信息吗?这种交互给用户带来的体验非常差。 为了解决这个问题,我们通常在接口中捕获异常:@GetMapping("/add") public String add() { String result = "success"; try { int a = 10 / 0; } catch (Exception e) { result = "error"; } return result; }界面修改后,出现异常时会提示:“数据异常”,更加人性化。看起来不错,但是有一个问题。如果只是一个接口还好,但是如果项目中有成百上千个接口,还得加异常捕获代码吗?答案是否定的,这就是全局异常处理派上用场的地方:RestControllerAdvice。@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public String handleException(Exception e) { if (e instanceof ArithmeticException) { return "data error"; } if (e instanceof Exception) { return "service error"; } retur null; } }方法中处理异常只需要handleException,在业务接口中就可以安心使用,不再需要捕获异常(统一有人处理)。4. 自定义拦截器Spring MVC拦截器,它可以获得HttpServletRequest和HttpServletResponse等web对象实例。Spring MVC拦截器的顶层接口是HandlerInterceptor,它包含三个方法:preHandle 在目标方法执行之前执行执行目标方法后执行的postHandleafterCompletion 在请求完成时执行为了方便,我们一般继承HandlerInterceptorAdapter,它实现了HandlerInterceptor。如果有授权鉴权、日志、统计等场景,可以使用该拦截器,我们来演示下吧。写一个类继承HandlerInterceptorAdapter:public class AuthInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestUrl = request.getRequestURI(); if (checkAuth(requestUrl)) { return true; } return false; } private boolean checkAuth(String requestUrl) { return true; } }将拦截器注册到spring容器中@Configuration public class WebAuthConfig extends WebMvcConfigurerAdapter { @Bean public AuthInterceptor getAuthInterceptor() { return new AuthInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()); } }Spring MVC在请求接口时可以自动拦截接口,并通过拦截器验证权限。5. 导入配置有时我们需要在某个配置类中引入其他的类,引入的类也加入到Spring容器中。 这时候可以使用注解@Import来完成这个功能。如果你查看它的源代码,你会发现导入的类支持三种不同的类型。但是我觉得最好把普通类的配置类和@Configuration注解分开解释,所以列出了四种不同的类型:5.1 通用类这种引入方式是最简单的,引入的类会被实例化为一个bean对象。public class A { } @Import(A.class) @Configuration public class TestConfiguration { }通过@Import注解引入类A,spring可以自动实例化A对象,然后在需要使用的地方通过注解@Autowired注入:@Autowired private A a;5.2 配置类这种引入方式是最复杂的,因为@Configuration支持还支持多种组合注解,比如:@Import @ImportResource @PropertySource public class A { } public class B { } @Import(B.class) @Configuration public class AConfiguration { @Bean public A a() { return new A(); } } @Import(AConfiguration.class) @Configuration public class TestConfiguration { }@Configuration注解的配置类通过@Import注解导入,配置类@Import、@ImportResource相关注解引入的类会一次性全部递归引入@PropertySource所在的属性。5.3 ImportSelector该导入方法需要实现ImportSelector接口public class AImportSelector implements ImportSelector { private static final String CLASS_NAME = "com.sue.cache.service.test13.A"; public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{CLASS_NAME}; } } @Import(AImportSelector.class) @Configuration public class TestConfiguration { }这种方法的好处是selectImports方法返回的是一个数组,也就是说可以同时引入多个类,非常方便。5.4 ImportBeanDefinitionRegistrar该导入方法需要实现ImportBeanDefinitionRegistrar接口:public class AImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(A.class); registry.registerBeanDefinition("a", rootBeanDefinition); } } @Import(AImportBeanDefinitionRegistrar.class) @Configuration public class TestConfiguration { }这种方法是最灵活的。 容器注册对象可以在registerBeanDefinitions方法中获取,可以手动创建BeanDefinition注册到BeanDefinitionRegistry种。6. 当工程启动时有时候我们需要在项目启动的时候自定义一些额外的功能,比如加载一些系统参数,完成初始化,预热本地缓存等。 我们应该做什么?好消息是 SpringBoot 提供了:CommandLineRunnerApplicationRunner这两个接口帮助我们实现了上面的需求。它们的用法很简单,以ApplicationRunner接口为例:@Component public class TestRunner implements ApplicationRunner { @Autowired private LoadDataService loadDataService; public void run(ApplicationArguments args) throws Exception { loadDataService.load(); } }实现ApplicationRunner接口,重写run方法,在该方法中实现您的自定义需求。如果项目中有多个类实现了ApplicationRunner接口,如何指定它们的执行顺序?答案是使用@Order(n)注解,n的值越小越早执行。 当然,顺序也可以通过@Priority注解来指定。7. 修改BeanDefinition在实例化Bean对象之前,Spring IOC需要读取Bean的相关属性,保存在BeanDefinition对象中,然后通过BeanDefinition对象实例化Bean对象。如果要修改BeanDefinition对象中的属性怎么办?答案:我们可以实现 BeanFactoryPostProcessor 接口。@Component public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory; BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(User.class); beanDefinitionBuilder.addPropertyValue("id", 123); beanDefinitionBuilder.addPropertyValue("name", "Tom"); defaultListableBeanFactory.registerBeanDefinition("user", beanDefinitionBuilder.getBeanDefinition()); } }在postProcessBeanFactory方法中,可以获取BeanDefinition的相关对象,修改对象的属性。8. 初始化 Bean 前和后有时,您想在 bean 初始化前后实现一些您自己的逻辑。这时候就可以实现:BeanPostProcessor接口。该接口目前有两个方法:postProcessBeforeInitialization:应该在初始化方法之前调用。postProcessAfterInitialization:此方法在初始化方法之后调用。@Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof User) { ((User) bean).setUserName("Tom"); } return bean; } }我们经常使用的@Autowired、@Value、@Resource、@PostConstruct等注解都是通过AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor来实现的。9. 初始化方法目前在Spring中初始化bean的方式有很多种:使用@PostConstruct注解实现InitializingBean接口9.1 使用@PostConstruct@Service public class AService { @PostConstruct public void init() { System.out.println("===init==="); } }为需要初始化的方法添加注解@PostConstruct,使其在Bean初始化时执行。9.2 实现初始化接口InitializingBean@Service public class BService implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { System.out.println("===init==="); } }实现InitializingBean接口,重写afterPropertiesSet方法,在该方法中可以完成初始化功能。10. 关闭Spring容器前有时候,我们需要在关闭spring容器之前做一些额外的工作,比如关闭资源文件。此时你可以实现 DisposableBean 接口并重写它的 destroy 方法。@Service public class DService implements InitializingBean, DisposableBean { @Override public void destroy() throws Exception { System.out.println("DisposableBean destroy"); } @Override public void afterPropertiesSet() throws Exception { System.out.println("InitializingBean afterPropertiesSet"); } }这样,在spring容器销毁之前,会调用destroy方法做一些额外的工作。通常我们会同时实现InitializingBean和DisposableBean接口,重写初始化方法和销毁方法。11. 自定义Bean的scope我们都知道spring core默认只支持两种Scope:Singleton单例,从spring容器中获取的每一个bean都是同一个对象。prototype多实例,每次从spring容器中获取的bean都是不同的对象。Spring Web 再次扩展了 Scope,添加RequestScope:同一个请求中从spring容器中获取的bean都是同一个对象。SessionScope:同一个session从spring容器中获取的bean都是同一个对象。尽管如此,有些场景还是不符合我们的要求。比如我们在同一个线程中要从spring容器中获取的bean都是同一个对象,怎么办?答案:这需要一个自定义范围。实现 Scope 接口public class ThreadLocalScope implements Scope { private static final ThreadLocal THREAD_LOCAL_SCOPE = new ThreadLocal(); @Override public Object get(String name, ObjectFactory<?> objectFactory) { Object value = THREAD_LOCAL_SCOPE.get(); if (value != null) { return value; } Object object = objectFactory.getObject(); THREAD_LOCAL_SCOPE.set(object); return object; } @Override public Object remove(String name) { THREAD_LOCAL_SCOPE.remove(); return null; } @Override public void registerDestructionCallback(String name, Runnable callback) { } @Override public Object resolveContextualObject(String key) { return null; } @Override public String getConversationId() { return null; } }将新定义的Scope注入到Spring容器中@Component public class ThreadLocalBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { beanFactory.registerScope("threadLocalScope", new ThreadLocalScope()); } }使用新定义的Scope@Scope("threadLocalScope") @Service public class CService { public void add() { } }总结本文总结了Spring中很常用的11个扩展点,可以在Bean创建、初始化到销毁各个阶段注入自己想要的逻辑,也有Spring MVC相关的拦截器等扩展点,希望对大家有帮助。
2023年01月17日
28 阅读
0 评论
0 点赞
2023-01-03
Jenkins+Docker 一键自动化部署 SpringBoot 项目
{mtitle title="安装Jenkins"/}1.安装Jenkinsdocker 安装一切都是那么简单,注意检查8080是否已经占用!如果占用修改端口docker run --name jenkins -u root --rm -d -p 8080:8080 -p 50000:50000 -v /var/jenkins_home:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock jenkinsci/blueocean如果没改端口号的话,安装完成后访问地址-> http://{部署Jenkins所在服务IP}:8080,此处会有几分钟的等待时间。{mtitle title="初始化Jenkins"/}解锁Jenkins进入Jenkins容器:docker exec -it {Jenkins容器名} bash例如 docker exec -it jenkins bash查看密码:cat /var/lib/jenkins/secrets/initialAdminPassword复制密码到输入框里面安装插件选择第一个安装推荐的插件创建管理员用户此账户一定要记住哦{mtitle title="系统配置"/}安装需要插件进入【首页】–【系统管理】–【插件管理】–【可选插件】搜索以下需要安装的插件,点击安装即可。安装Maven Integration安装Publish Over SSH(如果不需要远程推送,不用安装)如果使用Gitee 码云,安装插件Gitee(Git自带不用安装)配置Maven进入【首页】–【系统管理】–【全局配置】,拉到最下面maven–maven安装{mtitle title="创建任务"/}新建任务点击【新建任务】,输入任务名称,点击构建一个自由风格的软件项目源码管理点击【源码管理】–【Git】,输入仓库地址,添加凭证,选择好凭证即可。构建触发器点击【构建触发器】–【构建】–【增加构建步骤】–【调用顶层Maven目标】–【填写配置】–【保存】此处命令只是install,看是否能生成jar包clean install -Dmaven.test.skip=true{mtitle title="运行项目"/}因为我们项目和jenkins在同一台服务器,所以我们用shell脚本运行项目,原理既是通过dockerfile 打包镜像,然后docker运行即可。Dockerfile在springboot项目根目录新建一个名为Dockerfile的文件,注意没有后缀名,其内容如下:(大致就是使用jdk8,把jar包添加到docker然后运行prd配置文件。详细可以查看其他教程)FROM jdk:8 VOLUME /tmp ADD target/zx-order-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8888 ENTRYPOINT ["Bash","-DBash.security.egd=file:/dev/./urandom","-jar","/app.jar","--spring.profiles.active=prd"]修改jenkins任务配置配置如下:-t:指定新镜像名.:表示Dockfile在当前路径cd /var/jenkins_home/workspace/zx-order-api docker stop zx-order || true docker rm zx-order || true docker rmi zx-order || true docker build -t zx-order . docker run -d -p 8888:8888 --name zx-order zx-order:latest备注:上图用了docker logs -f 是为了方便看日志,真实不要用,因为会一直等待日志,构建任务会失败加|| true 是如果命令执行失败也会继续实行,为了防止第一次没有该镜像报错构建验证docker ps 查看是否有自己的容器docker logs 自己的容器名 查看日志是否正确浏览器访问项目试一试
2023年01月03日
30 阅读
0 评论
0 点赞
2022-12-13
瞧瞧人家写的API接口代码,那叫一个优雅!
{mtitle title="前言"/} 在实际工作中,我们需要经常跟第三方平台打交道,可能会对接第三方平台API接口,或者提供API接口给第三方平台调用。那么问题来了,如何设计一个优雅的API接口,能够满足:安全性、可重复调用、稳定性、好定位问题等多方面需求? 今天跟大家一起聊聊设计API接口时,需要注意的一些地方,希望对你会有所帮助。1. 签名 为了防止API接口中的数据被篡改,很多时候我们需要对API接口做签名。接口请求方将请求参数 + 时间戳 + 密钥拼接成一个字符串,然后通过md5等hash算法,生成一个前面sign。然后在请求参数或者请求头中,增加sign参数,传递给API接口。API接口的网关服务,获取到该sign值,然后用相同的请求参数 + 时间戳 + 密钥拼接成一个字符串,用相同的m5算法生成另外一个sign,对比两个sign值是否相等。如果两个sign相等,则认为是有效请求,API接口的网关服务会将给请求转发给相应的业务系统。如果两个sign不相等,则API接口的网关服务会直接返回签名错误。 问题来了:签名中为什么要加时间戳? 答:为了安全性考虑,防止同一次请求被反复利用,增加了密钥没破解的可能性,我们必须要对每次请求都设置一个合理的过期时间,比如:15分钟。这样一次请求,在15分钟之内是有效的,超过15分钟,API接口的网关服务会返回超过有效期的异常提示。目前生成签名中的密钥有两种形式:一种是双方约定一个固定值privateKey。另一种是API接口提供方给出AK/SK两个值,双方约定用SK作为签名中的密钥。AK接口调用方作为header中的accessKey传递给API接口提供方,这样API接口提供方可以根据AK获取到SK,而生成新的sgin。2. 加密 有些时候,我们的API接口直接传递的非常重要的数据,比如:用户的银行卡号、转账金额、用户身份证等,如果将这些参数,直接明文,暴露到公网上是非常危险的事情。由此,我们需要对数据进行加密。目前使用比较多的是用BASE64加解密。我们可以将所有的数据,安装一定的规律拼接成一个大的字符串,然后在加一个密钥,拼接到一起。然后使用JDK1.8之后的Base64工具类处理,效果如下: 【加密前的数据】www.baidu.com【加密后的数据】d3d3LmJhaWR1LmNvbQ==为了安全性,使用Base64可以加密多次。 API接口的调用方在传递参数时,body中只有一个参数data,它就是base64之后的加密数据。API接口的网关服务,在接收到data数据后,根据双方事先预定的密钥、加密算法、加密次数等,进行解密,并且反序列化出参数数据。3. ip白名单 为了进一步加强API接口的安全性,防止接口的签名或者加密被破解了,攻击者可以在自己的服务器上请求该接口。需求限制请求ip,增加ip白名单。只有在白名单中的ip地址,才能成功请求API接口,否则直接返回无访问权限。ip白名单也可以加在API网关服务上。但也要防止公司的内部应用服务器被攻破,这种情况也可以从内部服务器上发起API接口的请求。时候就需要增加web防火墙了,比如:ModSecurity等。4. 限流 如果你的API接口被第三方平台调用了,这就意味着着,调用频率是没法控制的。第三方平台调用你的API接口时,如果并发量一下子太高,可能会导致你的API服务不可用,接口直接挂掉。由此,必须要对API接口做限流。限流方法有三种: 对请求ip做限流:比如同一个ip,在一分钟内,对API接口总的请求次数,不能超过10000次。对请求接口做限流:比如同一个ip,在一分钟内,对指定的API接口,请求次数不能超过2000次。对请求用户做限流:比如同一个AK/SK用户,在一分钟内,对API接口总的请求次数,不能超过10000次。我们在实际工作中,可以通过nginx,redis或者gateway实现限流的功能。5. 参数校验 我们需要对API接口做参数校验,比如:校验必填字段是否为空,校验字段类型,校验字段长度,校验枚举值等等。这样做可以拦截一些无效的请求。比如在新增数据时,字段长度超过了数据字段的最大长度,数据库会直接报错。但这种异常的请求,我们完全可以在API接口的前期进行识别,没有必要走到数据库保存数据那一步,浪费系统资源。有些金额字段,本来是正数,但如果用户传入了负数,万一接口没做校验,可能会导致一些没必要的损失。还有些状态字段,如果不做校验,用户如果传入了系统中不存在的枚举值,就会导致保存的数据异常。由此可见,做参数校验是非常有必要的。在Java中校验数据使用最多的是hiberate的Validator框架,它里面包含了@Null、@NotEmpty、@Size、@Max、@Min等注解。用它们校验数据非常方便。 当然有些日期字段和枚举字段,可能需要通过自定义注解的方式实现参数校验。6. 统一返回值我之前调用过别人的API接口,正常返回数据是一种json格式,比如:{ "code":0, "message":null, "data":[{"id":123,"name":"abc"}] }签名错误返回的json格式:{ "code":1001, "message":"签名错误", "data":null }没有数据权限返回的json格式:{ "rt":10, "errorMgt":"没有权限", "result":null } 这种是比较坑的做法,返回值中有多种不同格式的返回数据,这样会导致对接方很难理解。出现这种情况,可能是API网关定义了一直返回值结构,业务系统定义了另外一种返回值结构。如果是网关异常,则返回网关定义的返回值结构,如果是业务系统异常,则返回业务系统的返回值结构。但这样会导致API接口出现不同的异常时,返回不同的返回值结构,非常不利于接口的维护。其实这个问题我们可以在设计API网关时解决。业务系统在出现异常时,抛出业务异常的RuntimeException,其中有个message字段定义异常信息。所有的API接口都必须经过API网关,API网关捕获该业务异常,然后转换成统一的异常结构返回,这样能统一返回值结构。7. 统一封装异常 我们的API接口需要对异常进行统一处理。不知道你有没有遇到过这种场景:有时候在API接口中,需要访问数据库,但表不存在,或者sql语句异常,就会直接把sql信息在API接口中直接返回。返回值中包含了异常堆栈信息、数据库信息、错误代码和行数等信息。如果直接把这些内容暴露给第三方平台,是很危险的事情。有些不法分子,利用接口返回值中的这些信息,有可能会进行sql注入或者直接脱库,而对我们系统造成一定的损失。因此非常有必要对API接口中的异常做统一处理,把异常转换成这样:{ "code":500, "message":"服务器内部错误", "data":null } 返回码code是500,返回信息message是服务器内部异常。这样第三方平台就知道是API接口出现了内部问题,但不知道具体原因,他们可以找我们排查问题。我们可以在内部的日志文件中,把堆栈信息、数据库信息、错误代码行数等信息,打印出来。我们可以在gateway中对异常进行拦截,做统一封装,然后给第三方平台的是处理后没有敏感信息的错误信息。8. 请求日志 在第三方平台请求你的API接口时,接口的请求日志非常重要,通过它可以快速的分析和定位问题。我们需要把API接口的请求url、请求参数、请求头、请求方式、响应数据和响应时间等,记录到日志文件中。最好有traceId,可以通过它串联整个请求的日志,过滤多余的日志。当然有些时候,请求日志不光是你们公司开发人员需要查看,第三方平台的用户也需要能查看接口的请求日志。这时就需要把日志落地到数据库,比如:mongodb或者elastic search,然后做一个UI页面,给第三方平台的用户开通查看权限。这样他们就能在外网查看请求日志了,他们自己也能定位一部分问题。9. 幂等设计 第三方平台极有可能在极短的时间内,请求我们接口多次,比如:在1秒内请求两次。有可能是他们业务系统有bug,或者在做接口调用失败重试,因此我们的API接口需要做幂等设计。也就是说要支持在极短的时间内,第三方平台用相同的参数请求API接口多次,第一次请求数据库会新增数据,但第二次请求以后就不会新增数据,但也会返回成功。这样做的目的是不会产生错误数据。我们在日常工作中,可以通过在数据库中增加唯一索引,或者在redis保存requestId和请求参来保证接口幂等性。对接口幂等性感兴趣的小伙伴,可以看看我的另一篇文章《高并发下如何保证接口的幂等性?》,里面有非常详细的介绍。10. 限制记录条数 对于对我提供的批量接口,一定要限制请求的记录条数。如果请求的数据太多,很容易造成API接口超时等问题,让API接口变得不稳定。通常情况下,建议一次请求中的参数,最多支持传入500条记录。如果用户传入多余500条记录,则接口直接给出提示。建议这个参数做成可配置的,并且要事先跟第三方平台协商好,避免上线后产生不必要的问题。11. 压测 上线前我们务必要对API接口做一下压力测试,知道各个接口的qps情况。以便于我们能够更好的预估,需要部署多少服务器节点,对于API接口的稳定性至关重要。之前虽说对API接口做了限流,但是实际上API接口是否能够达到限制的阀值,这是一个问号,如果不做压力测试,是有很大风险的。比如:你API接口限流1秒只允许50次请求,但实际API接口只能处理30次请求,这样你的API接口也会处理不过来。我们在工作中可以用jmeter或者apache benc对API接口做压力测试。12. 异步处理 一般的API接口的逻辑都是同步处理的,请求完之后立刻返回结果。但有时候,我们的API接口里面的业务逻辑非常复杂,特别是有些批量接口,如果同步处理业务,耗时会非常长。这种情况下,为了提升API接口的性能,我们可以改成异步处理。在API接口中可以发送一条mq消息,然后直接返回成功。之后,有个专门的mq消费者去异步消费该消息,做业务逻辑处理。直接异步处理的接口,第三方平台有两种方式获取到。第一种方式是:我们回调第三方平台的接口,告知他们API接口的处理结果,很多支付接口就是这么玩的。第二种方式是:第三方平台通过轮询调用我们另外一个查询状态的API接口,每隔一段时间查询一次状态,传入的参数是之前的那个API接口中的id集合。13. 数据脱敏 有时候第三方平台调用我们API接口时,获取的数据中有一部分是敏感数据,比如:用户手机号、银行卡号等等。这样信息如果通过API接口直接保留到外网,是非常不安全的,很容易造成用户隐私数据泄露的问题。这就需要对部分数据做数据脱敏了。我们可以在返回的数据中,部分内容用星号代替。已用户手机号为例:182**887。这样即使数据被泄露了,也只泄露了一部分,不法分子拿到这份数据也没啥用。14. 完整的接口文档 说实话,一份完整的API接口文档,在双方做接口对接时,可以减少很多沟通成本,让对方少走很多弯路。 接口文档中需要包含如下信息: 接口地址请求方式,比如:post或get请求参数和字段介绍返回值和字段介绍返回码和错误信息加密或签名示例完整的请求demo额外的说明,比如:开通ip白名单。接口文档中最好能够统一接口和字段名称的命名风格,比如都用驼峰标识命名。接口地址中可以加一个版本号v1,比如:v1/query/getCategory,这样以后接口有很大的变动,可以非常方便升级版本。统一字段的类型和长度,比如:id字段用Long类型,长度规定20。status字段用int类型,长度固定2等。统一时间格式字段,比如:time用String类型,格式为:yyyy-MM-dd HH:mm:ss。接口文档中写明AK/SK和域名,找某某单独提供等。
2022年12月13日
29 阅读
0 评论
0 点赞
2022-12-13
Java 中九种 Map 的遍历方式,你一般用的是哪种呢?
{mtitle title="通过 entrySet 来遍历"/}1、通过 for 和 map.entrySet() 来遍历 第一种方式是采用 for 和 Map.Entry 的形式来遍历,通过遍历 map.entrySet() 获取每个 entry 的 key 和 value,代码如下。这种方式一般也是阿粉使用的比较多的一种方式,没有什么花里胡哨的用法,就是很朴素的获取 map 的 key 和 value。public static void testMap1(Map<Integer, Integer> map) { long sum = 0; for (Map.Entry<Integer, Integer> entry : map.entrySet()) { sum += entry.getKey() + entry.getValue(); } System.out.println(sum); }看过 HashMap 源码的同学应该会发现,这个遍历方式在源码中也有使用,如下图所示,putMapEntries 方法在我们调用 putAll 方法的时候会用到。2、通过 for, Iterator 和 map.entrySet() 来遍历 我们第一个方法是直接通过 for 和 entrySet() 来遍历的,这次我们使用 entrySet() 的迭代器来遍历,代码如下。public static void testMap2(Map<Integer, Integer> map) { long sum = 0; for (Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator(); entries.hasNext(); ) { Map.Entry<Integer, Integer> entry = entries.next(); sum += entry.getKey() + entry.getValue(); } System.out.println(sum); }3、通过 while,Iterator 和 map.entrySet() 来遍历上面的迭代器是使用 for 来遍历,那我们自然可以想到还可以用 while 来进行遍历,所以代码如下所示。 public static void testMap3(Map<Integer, Integer> map) { Iterator<Map.Entry<Integer, Integer>> it = map.entrySet().iterator(); long sum = 0; while (it.hasNext()) { Map.Entry<Integer, Integer> entry = it.next(); sum += entry.getKey() + entry.getValue(); } System.out.println(sum); }这种方法跟上面的方法类似,只不过循环从 for 换成了 while,日常我们在开发的时候,很多场景都可以将 for 和 while 进行替换。2 和 3 都使用迭代器 Iterator,通过迭代器的 next(),方法来获取下一个对象,依次判断是否有 next。{mtitle title="通过 keySet 来遍历"/}上面的这三种方式虽然代码的写法不同,但是都是通过遍历 map.entrySet() 来获取结果的,殊途同归。接下来我们看另外的一组。4、通过 for 和 map.keySet() 来遍历前面的遍历是通过 map.entrySet() 来遍历,这里我们通过 map.keySet() 来遍历,顾名思义前者是保存 entry 的集合,后者是保存 key 的集合,遍历的代码如下,因为是 key 的集合,所以如果想要获取 key 对应的 value 的话,还需要通过 map.get(key) 来获取。public static void testMap4(Map<Integer, Integer> map) { long sum = 0; for (Integer key : map.keySet()) { sum += key + map.get(key); } System.out.println(sum); }5、通过 for,Iterator 和 map.keySet() 来遍历public static void testMap5(Map<Integer, Integer> map) { long sum = 0; for (Iterator<Integer> key = map.keySet().iterator(); key.hasNext(); ) { Integer k = key.next(); sum += k + map.get(k); } System.out.println(sum); }6、通过 while,Iterator 和 map.keySet() 来遍历public static void testMap6(Map<Integer, Integer> map) { Iterator<Integer> it = map.keySet().iterator(); long sum = 0; while (it.hasNext()) { Integer key = it.next(); sum += key + map.get(key); } System.out.println(sum); }我们可以看到这种方式相对于 map.entrySet() 方式,多了一步 get 的操作,这种场景比较适合我们只需要 key 的场景,如果也需要使用 value 的场景不建议使用 map.keySet() 来进行遍历,因为会多一步 map.get() 的操作。{mtitle title="Java 8 的遍历方式"/}注意下面的几个遍历方法都是是 JDK 1.8 引入的,如果使用的 JDK 版本不是 1.8 以及之后的版本的话,是不支持的。7、通过 map.forEach() 来遍历JDK 中的 forEach 方法,使用率也挺高的。public static void testMap7(Map<Integer, Integer> map) { final long[] sum = {0}; map.forEach((key, value) -> { sum[0] += key + value; }); System.out.println(sum[0]); }该方法被定义在 java.util.Map#forEach 中,并且是通过 default 关键字来标识的,如下图所示。这里提个问题,为什么要使用 default 来标识呢?欢迎把你的答案写在评论区。8、Stream 遍历public static void testMap8(Map<Integer, Integer> map) { long sum = map.entrySet().stream().mapToLong(e -> e.getKey() + e.getValue()).sum(); System.out.println(sum); }9、ParallelStream 遍历 public static void testMap9(Map<Integer, Integer> map) { long sum = map.entrySet().parallelStream().mapToLong(e -> e.getKey() + e.getValue()).sum(); System.out.println(sum); }这两种遍历方式都是 JDK 8 的 Stream 遍历方式,stream 是普通的遍历,parallelStream 是并行流遍历,在某些场景会提升性能,但是也不一定。测试代码上面的遍历方式有了,那么我们在日常开发中到底该使用哪一种呢?每一种的性能是怎么样的呢?为此阿粉这边通过下面的代码,我们来测试一下每种方式的执行时间。public static void main(String[] args) { int outSize = 1; int mapSize = 200; Map<Integer, Integer> map = new HashMap<>(mapSize); for (int i = 0; i < mapSize; i++) { map.put(i, i); } System.out.println("---------------start------------------"); long totalTime = 0; for (int size = outSize; size > 0; size--) { long startTime = System.currentTimeMillis(); testMap1(map); totalTime += System.currentTimeMillis() - startTime; } System.out.println("testMap1 avg time is :" + (totalTime / outSize)); // 省略其他方法,代码跟上面一致 }为了避免一些干扰,这里通过外层的 for 来进行多次计算,然后求平均值,当我们的参数分别是 outSize = 1,mapSize = 200 的时候,测试的结果如下当随着我们增大 mapSize 的时候,我们会发现,后面几个方法的性能是逐渐上升的。总结从上面的例子来看,当我们的集合数量很少的时候,基本上普通的遍历就可以搞定,不需要使用 JDK 8 的高级 API 来进行遍历,当我们的集合数量较大的时候,就可以考虑采用 JDK 8 的 forEach 或者 Stream 来进行遍历,这样的话效率更高。在普通的遍历方法中 entrySet() 的方法要比使用 keySet() 的方法好。
2022年12月13日
26 阅读
0 评论
0 点赞
2022-11-25
数据权限的设计思路
数据权限,主要进行数据的过滤,没相应的数据权限,则不能查询、操作相应的数据。数据权限实现方式如下:用户登录后,获取部门 ID 数据权限列表根据部门 ID 列表进行数据过滤,数据过滤表,都需要有部门 ID 字段一、自定义权限过滤注解@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataFilter { /** * 表的别名 */ String tableAlias() default ""; /** * 用户ID */ String userId() default "creator"; /** * 部门ID */ String deptId() default "dept_id"; }二、数据过滤,切面处理类@Aspect @Component public class DataFilterAspect { @Before("dataFilterCut(datafilter)") public void dataFilter(JoinPoint point,DataFilter datafilter) { Object params = point.getArgs()[0]; if(params != null && params instanceof Map){ UserDetail user = SecurityUser.getUser(); //如果是超级管理员或超级租户,则不进行数据过滤 if(user.getSuperAdmin() == SuperAdminEnum.YES.value() || user.getSuperTenant() == SuperTenantEnum.YES.value()) { return ; } try { //否则进行数据过滤 Map map = (Map)params; String sqlFilter = getSqlFilter(user, point); map.put(Constant.SQL_FILTER, new DataScope(sqlFilter)); }catch (Exception e){ } return ; } throw new RenException(ErrorCode.DATA_SCOPE_PARAMS_ERROR); } /** * 获取数据过滤的SQL */ private String getSqlFilter(UserDetail user, JoinPoint point) throws Exception { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = point.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getParameterTypes()); DataFilter dataFilter = method.getAnnotation(DataFilter.class); //获取表的别名 String tableAlias = dataFilter.tableAlias(); if(StringUtils.isNotBlank(tableAlias)){ tableAlias += "."; } StringBuilder sqlFilter = new StringBuilder(); sqlFilter.append(" ("); //部门ID列表 List<Long> deptIdList = user.getDeptIdList(); if(CollUtil.isNotEmpty(deptIdList)){ sqlFilter.append(tableAlias).append(dataFilter.deptId()); sqlFilter.append(" in(").append(StringUtils.join(deptIdList, ",")).append(")"); } //查询本人数据 if(CollUtil.isNotEmpty(deptIdList)){ sqlFilter.append(" or "); } sqlFilter.append(tableAlias).append(dataFilter.userId()).append("=").append(user.getId()); sqlFilter.append(")"); return sqlFilter.toString(); } }三、数据过滤public class DataFilterInterceptor implements InnerInterceptor { @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { DataScope scope = getDataScope(parameter); // 不进行数据过滤 if(scope == null || StrUtil.isBlank(scope.getSqlFilter())){ return; } // 拼接新SQL String buildSql = getSelect(boundSql.getSql(), scope); // 重写SQL PluginUtils.mpBoundSql(boundSql).sql(buildSql); } private DataScope getDataScope(Object parameter){ if (parameter == null){ return null; } // 判断参数里是否有DataScope对象 if (parameter instanceof Map) { Map<?, ?> parameterMap = (Map<?, ?>) parameter; for (Map.Entry entry : parameterMap.entrySet()) { if (entry.getValue() != null && entry.getValue() instanceof DataScope) { return (DataScope) entry.getValue(); } } } else if (parameter instanceof DataScope) { return (DataScope) parameter; } return null; } private String getSelect(String buildSql, DataScope scope){ try { Select select = (Select) CCJSqlParserUtil.parse(buildSql); PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); Expression expression = plainSelect.getWhere(); if(expression == null){ plainSelect.setWhere(new StringValue(scope.getSqlFilter())); }else{ AndExpression andExpression = new AndExpression(expression, new StringValue(scope.getSqlFilter())); plainSelect.setWhere(andExpression); } return select.toString().replaceAll("'", ""); }catch (JSQLParserException e){ return buildSql; } } }四、配置类@Configuration public class DataScopeConfig { @Bean public InnerInterceptor dataFilterInterceptor() { return new DataFilterInterceptor(); } }
2022年11月25日
55 阅读
0 评论
1 点赞
2022-08-28
SpringCloud使用注解+AOP+MQ来实现日志管理模块
{mtitle title="简介"/}无论在什么系统中,日志管理模块都属于十分重要的部分,接下来会通过注解+AOP+MQ的方式实现一个简易的日志管理系统。{mtitle title="思路"/}注解: 标记需要记录日志的方法AOP: 通过AOP增强代码,利用后置/异常通知的方式获取相关日志信息,最后使用MQ将日志信息发送到专门处理日志的系统RabbitMQ: 利用解耦、异步的特性,协调完成各个微服务系统之间的通信1、日志表结构表结构(sys_log):CREATE TABLE `sys_log` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '唯一ID', `opt_id` int(11) DEFAULT NULL COMMENT '操作用户id', `opt_name` varchar(50) DEFAULT NULL COMMENT '操作用户名', `log_type` varchar(20) DEFAULT NULL COMMENT '日志类型', `log_message` varchar(255) DEFAULT NULL COMMENT '日志信息(具体方法名)', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8 COMMENT='系统日志表';实体类(SysLog):@Data public class SysLog { private static final long serialVersionUID = 1L; /** * 唯一ID */ @TableId(value = "id", type = IdType.AUTO) private Integer id; /** * 操作用户id */ private Integer optId; /** * 操作用户名 */ private String optName; /** * 日志类型 */ private String logType; /** * 日志信息(具体方法名) */ private String logMessage; /** * 创建时间 */ private Date createTime; }2、注解注解(SystemLog):仅作为标记的作用,目的让JVM可以识别,然后可以从中获取相关信息@Target: 定义注解作用的范围,这里是方法@Retention: 定义注解生命周期,这里是运行时@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface SystemLog { SystemLogEnum type(); }枚举(SystemLogEnum):限定日志类型范围public enum SystemLogEnum { SAVE_LOG("保存"), DELETE_LOG("删除"), REGISTER_LOG("注册"), LOGIN_LOG("登录"), LAUD_LOG("点赞"), COLLECT_LOG("收藏"), THROW_LOG("异常"), ; private String type; SystemLogEnum(String type) { this.type = type; } public String getType() { return type; } }3、AOP切面AOP(SysLogAspect):实现代码的增强,主要通过动态代理方式实现的代码增强。拦截注解,并获取拦截到的相关信息,封装成日志对象发送到MQ队列(生产端)@Component @Aspect @Slf4j public class SysLogAspect { @Autowired MqStream stream; //切点 @Pointcut("@annotation(cn.zdxh.commons.utils.SystemLog)") public void logPointcut(){} //后置通知 @After("logPointcut()") public void afterLog(JoinPoint joinPoint) { //一般日志 SysLog sysLog = wrapSysLog(joinPoint); log.info("Log值:"+sysLog); //发送mq消息 stream.logOutput().send(MessageBuilder.withPayload(sysLog).build()); } //异常通知 @AfterThrowing(value = "logPointcut()", throwing = "e") public void throwingLog(JoinPoint joinPoint, Exception e) { //异常日志 SysLog sysLog = wrapSysLog(joinPoint); sysLog.setLogType(SystemLogEnum.THROW_LOG.getType()); sysLog.setLogMessage(sysLog.getLogMessage()+"==="+e); log.info("异常Log值:"+sysLog); //发送mq消息 stream.logOutput().send(MessageBuilder.withPayload(sysLog).build()); } /** * 封装SysLog对象 * @param joinPoint * @return */ public SysLog wrapSysLog(JoinPoint joinPoint){ //获取请求响应对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); MethodSignature signature = (MethodSignature)joinPoint.getSignature(); SysLog sysLog = new SysLog(); //获取方法全路径 String methodName = signature.getDeclaringTypeName()+"."+signature.getName(); //获取注解参数值 SystemLog systemLog = signature.getMethod().getAnnotation(SystemLog.class); //从header取出token String token = request.getHeader("token"); if (!StringUtils.isEmpty(token)) { //操作人信息 Integer userId = JwtUtils.getUserId(token); String username = JwtUtils.getUsername(token); sysLog.setOptId(userId); sysLog.setOptName(username); } if (!StringUtils.isEmpty(systemLog.type())){ sysLog.setLogType(systemLog.type().getType()); } sysLog.setLogMessage(methodName); sysLog.setCreateTime(new Date()); return sysLog; } }3、RabbitMQ消息队列MQ: 这里主要是通过Spring Cloud Stream集成的RabbitMQSpring Cloud Stream: 作为MQ的抽象层,已屏蔽各种MQ的各自名词,统称为input、output两大块。可以更方便灵活地切换各种MQ,如 kafka、RocketMQ等 (1)定义Input/Ouput接口(MqStream)@Component public interface MqStream { String LOG_INPUT = "log_input"; String LOG_OUTPUT = "log_output"; @Input(LOG_INPUT) SubscribableChannel logInput(); @Output(LOG_OUTPUT) MessageChannel logOutput(); }(2)MQ生产者注:这里使用到AOP切面的微服务,都属于MQ生产者服务引入依赖: 这里没有版本号的原因是spring cloud已经帮我们管理好各个版本号,已无需手动定义版本号<!--Spring Cloud Stream--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-binder-rabbit</artifactId> </dependency>在程序入口开启MQ的Input/Output绑定:@SpringBootApplication(scanBasePackages = {"cn.zdxh.user","cn.zdxh.commons"}) @EnableEurekaClient @MapperScan("cn.zdxh.user.mapper") @EnableBinding(MqStream.class) //开启绑定 @EnableFeignClients public class YouquServiceProviderUserApplication { public static void main(String[] args) { SpringApplication.run(YouquServiceProviderUserApplication.class, args); } }yml配置: 在生产者端设置outputdestination: 相当于rabbitmq的exchangegroup: 相当于rabbitmq的queue,不过是和destination一起组合成的queue名binder: 需要绑定的MQ#Spring Cloud Stream相关配置 spring: cloud: stream: bindings: # exchange与queue绑定 log_output: # 日志生产者设置output destination: log.exchange content-type: application/json group: log.queue binder: youqu_rabbit #自定义名称 binders: youqu_rabbit: #自定义名称 type: rabbit environment: spring: rabbitmq: host: localhost port: 5672 username: guest password: 25802580注:完成以上操作,即完成MQ生产端的所有工作(3)MQ消费者 引入依赖、开启Input/Output绑定:均和生产者的设置一致yml配置:在生产者端设置inputspring: cloud: # Spring Cloud Stream 相关配置 stream: bindings: # exchange与queue绑定 log_input: # 日志消费者设置input destination: log.exchange content-type: application/json group: log.queue binder: youqu_rabbit binders: youqu_rabbit: type: rabbit environment: spring: rabbitmq: host: localhost port: 5672 username: guest password: 25802580消费者监听(LogMqListener): 监听生产者发过来的日志信息,将信息添加到数据库即可@Service @Slf4j public class LogMqListener { @Autowired SysLogService sysLogService; @StreamListener(MqStream.LOG_INPUT) public void input(SysLog sysLog) { log.info("开始记录日志========================"); sysLogService.save(sysLog); log.info("结束记录日志========================"); } }注:完成以上操作,即完成MQ消费端的所有工作4、应用 简述:只需将@SystemLog(type = SystemLogEnum.REGISTER_LOG),标记在需要记录的方法上,当有客户端访问该方法时,就可以自动完成日志的记录5、总结 流程:注解标记--->AOP拦截--->日志发送到MQ--->专门处理日志的系统监听MQ消息 --->日志插入到数据库
2022年08月28日
50 阅读
0 评论
0 点赞
2022-08-24
CompletableFuture异步工作编排
public static void main(String[] args) { CompletableFuture<List> stepOne = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } List<Integer> stepOneList = new ArrayList(); stepOneList.add(1); System.out.println("执行1结束"); return stepOneList; }); CompletableFuture<List> stepTwo = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } List<Integer> stepTwoList = new ArrayList(); stepTwoList.add(2); System.out.println("执行2结束"); return stepTwoList; }); CompletableFuture<List> stepThree = CompletableFuture.supplyAsync(() -> { List<Integer> stepThreeList = new ArrayList(); stepThreeList.add(3); System.out.println("执行3结束"); return stepThreeList; }); System.out.println("执行4结束"); CompletableFuture<Void> all = CompletableFuture.allOf(stepOne, stepTwo, stepThree); all.join(); try { System.out.println(stepOne.get()); System.out.println(stepTwo.get()); System.out.println(stepThree.get()); } catch (Exception e){ } }
2022年08月24日
44 阅读
0 评论
0 点赞
2022-08-17
Spring Event + 异步实现业务的解耦(结合Spring Retry重试)
{mtitle title="使用场景"/}项目开发中经常会涉及到非常复杂的业务逻辑,如果全部耦合在一起,一个类的代码就会非常长,所以需要我们对业务代码进行解耦。通常在一个复杂的逻辑业务里面,会包含一个核心业务和N个子业务,有些子业务,我们是不需要在当前请求中同步完成的,例如短信发送等。这时,我们可能会想到MQ。但是当我们不想引入MQ时,就可以考虑使用Spring Event,它相当于一个观察者模式,当一个Bean完成任务之后,会通知另一个Bean去执行相应的任务。Spring Retry也是Spring里的一个组件,主要实现是发生异常后重新调用。当一些瞬时错误(例如网络问题)发生时,Spring Retry的重试可以帮我们避免这种异常造成的服务调用失败的情况。{mtitle title="使用示例"/}@Component @Slf4j public class MsgListener { /** * value值表示当哪些异常的时候触发重试, * maxAttempts表示最大重试次数默认为3, * delay表示重试的延迟时间, * multiplier表示上一次延时时间是这一次的倍数。 * @param event */ @EventListener @Async @Retryable(value = Exception.class,maxAttempts = 3,backoff = @Backoff(delay = 2000,multiplier = 1.5)) public void sendMsg(MsgEvent event) { String msgId = event.getMsgId(); StopWatch watch = new StopWatch(msgId); watch.start(); log.info("开始发短信"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } watch.stop(); log.info("短信发送成功, 消息id:【{}】 | 耗时: ({})", msgId, watch.getLastTaskTimeMillis()); } }@SpringBootApplication @EnableAsync @EnableRetry public class LabApplication { public static void main(String[] args) { SpringApplication.run(LabApplication.class, args); } }@SpringBootTest @Slf4j public class EventTest { @Autowired private ApplicationContext applicationContext; @Test public void msgTest() { applicationContext.publishEvent(new MsgEvent("123")); log.info("短信发送事件发布成功"); try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } } }
2022年08月17日
75 阅读
0 评论
0 点赞
2022-08-16
通过feign拦截器,实现用户态传递
/** * Feign 配置注册 * **/ @Configuration public class FeignAutoConfiguration { @Bean public RequestInterceptor requestInterceptor() { return new FeignRequestInterceptor(); } }/** * feign 请求拦截器 * */ @Component public class FeignRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { HttpServletRequest httpServletRequest = ServletUtils.getRequest(); if (StringUtils.isNotNull(httpServletRequest)) { Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest); // 传递用户信息请求头,防止丢失 String userId = headers.get(SecurityConstants.DETAILS_USER_ID); if (StringUtils.isNotEmpty(userId)) { requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId); } String userName = headers.get(SecurityConstants.DETAILS_USERNAME); if (StringUtils.isNotEmpty(userName)) { requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName); } String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER); if (StringUtils.isNotEmpty(authentication)) { requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication); } // 配置客户端IP requestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr(ServletUtils.getRequest())); } } }
2022年08月16日
75 阅读
0 评论
0 点赞
2022-07-31
spring为每个请求增加traceId
{mtitle title="前言"/} spring开发web项目经常会查看日志,通常都是根据每个http请求来查询整个链路的日志。有时这些请求的参数都差不多,日志也很相似,很难分辨出是否同一链路的请求。有时我会根据线程id来查询,但是tomcat的线程是复用的,同一个线程id对应多个请求链路日志,为此还是想办法为每个请求分配一个链路id。{mtitle title="原理"/}打标记:logback 在 SLF4J API 利用诊断上下文映射 (MDC)为每个请求打上唯一标记。例如:标记为traceId。使用标记:logback的pattern使用 %X{traceId}。打标记时机:HandlerInterceptor的preHandle方法清除标记:HandlerInterceptor的afterCompletion方法{mtitle title="实践"/}打标记/清除标记1、打标记【preHandle方法】 filter类:Interceptor String traceId = getTraceId(request); MDC.put("traceId", traceId); private String getTraceId(HttpServletRequest request){ return String.format("%s - %s",request.getRequestURI(), UUID.randomUUID()); }备注:filter需注 @Bean WebMvcConfigurer createWebMvcConfigurer(@Autowired HandlerInterceptor[] interceptors) { return new WebMvcConfigurer() { public void addInterceptors(InterceptorRegistry registry) { for (HandlerInterceptor interceptor : interceptors) { registry.addInterceptor(interceptor); } } }; } 2、清除标记【afterCompletion方法】MDC.clear();logback<?xml version="1.0" encoding="UTF-8"?> <configuration> <property name="LOG_FILE" value="god" /> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>./log/${LOG_FILE}.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 每日归档日志文件 --> <fileNamePattern>./log/${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern> <!-- 保留 30 天的归档日志文件 --> <maxHistory>30</maxHistory> <!-- 日志文件上限 3G,超过后会删除旧的归档日志文件 --> <totalSizeCap>100MB</totalSizeCap> </rollingPolicy> <encoder> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--> <pattern> %d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{50} %L - %msg%n</pattern> </encoder> </appender> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern> %d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{50} %L - %msg%n</pattern> </encoder> </appender> <logger name="com.xxx" level="debug" /> <root level="debug"> <!-- <appender-ref ref="console" />--> <appender-ref ref="FILE" /> </root> </configuration>备注:%X{traceId} : 标记的使用和输出。
2022年07月31日
80 阅读
0 评论
0 点赞
2022-07-08
驼峰与下划线互相转换(JS + JAVA)
{mtitle title="JS 版本"/}/** * 将驼峰转为下划线 */ function camelToUnderline(camelStr){ return camelStr.replace(/[A-Z]/g,function(s){ return ' '+s.toLowerCase(); }).trim().replaceAll(' ','_'); } /** * 下划线转小驼峰 */ function underlineToSmallCamel(str){ return str.toLowerCase().replace(/_([a-z])/g,function(s, s1){ return s1.toUpperCase(); }) } /** * 华氏温度(32℉) 转化为摄氏温度(0℃) */ function f2c(s) { return s.replace(/(\d+(\.\d*)?)℉/g, function($0,$1,$2) { return (($1-32) * 5/9) + '℃'; }); }{mtitle title="JAVA 版本"/}/** * */ package com; import java.util.HashMap; import java.util.Iterator; import java.util.Map; /** * @author xp test5.java 2018年12月27日 */ public class test5 { public static void main(String[] args) { //underscoreName("abcAbcaBc"); //camelName("abc_abca_bc"); //upcaseCamelName("abc_abca_bc"); Map<String, Object> data= new HashMap<String, Object>(); data.put("Abc_Abca_Bc_TEXT", ""); camelMap(data); //underscopeMap(data); } //驼峰转大写+下划线,abcAbcaBc->ABC_ABCA_BC public static String underscoreName(String name) { StringBuilder result = new StringBuilder(); if ((name != null) && (name.length() > 0)) { result.append(name.substring(0, 1).toUpperCase()); for (int i = 1; i < name.length(); i++) { String s = name.substring(i, i + 1); if ((s.equals(s.toUpperCase())) && (!Character.isDigit(s.charAt(0)))) { result.append("_"); } result.append(s.toUpperCase()); } } System.err.println("underscoreName:"+result.toString()); return result.toString(); } //下划线转驼峰,abc_abca_bc->abcAbcaBc public static String camelName(String name) { StringBuilder result = new StringBuilder(); if ((name == null) || (name.isEmpty())) { return ""; } if (!name.contains("_")) { return name.toLowerCase(); } String[] camels = name.split("_"); for (String camel : camels) { if (!camel.isEmpty()) { if (result.length() == 0) { result.append(camel.toLowerCase()); } else { result.append(camel.substring(0, 1).toUpperCase()); result.append(camel.substring(1).toLowerCase()); } } } System.err.println("camelName:"+result.toString()); return result.toString(); } //下划线转首字母大写驼峰,abc_abca_bc->AbcAbcaBc public static String upcaseCamelName(String name) { StringBuilder result = new StringBuilder(); if ((name == null) || (name.isEmpty())) { return ""; } if (!name.contains("_")) { result.append(name.substring(0, 1).toUpperCase()); result.append(name.substring(1).toLowerCase()); return result.toString(); } String[] camels = name.split("_"); for (String camel : camels) { if (!camel.isEmpty()) { result.append(camel.substring(0, 1).toUpperCase()); result.append(camel.substring(1).toLowerCase()); } } System.err.println("upcaseCamelName:"+result.toString()); return result.toString(); } public static Map<String, Object> camelMap(Map<String, Object> data) { if (data == null) { return null; } Map<String, Object> ret = new HashMap<>(); Iterator<String> keyIt = data.keySet().iterator(); while (keyIt.hasNext()) { String key = (String) keyIt.next(); ret.put(camelName(key), data.get(key)); if (key.endsWith("_TEXT")) { String key1 = key.substring(0, key.lastIndexOf("_")); ret.put(camelName(key1) + "_Text", data.get(key)); } } System.err.println("data:"+data); System.err.println("camelMap:"+ret); return ret; } public static Map<String, Object> underscopeMap(Map<String, Object> data) { if (data == null) { return null; } Map<String, Object> ret = new HashMap<>(); Iterator<String> keyIt = data.keySet().iterator(); while (keyIt.hasNext()) { String key = (String) keyIt.next(); ret.put(underscoreName(key), data.get(key)); } System.err.println("underscopeMap:"+ret); return ret; } }
2022年07月08日
98 阅读
0 评论
0 点赞
2022-07-06
通过URL获取各种参数
try { URL url = new URL("http://www.runoob.com/index.html?language=cn#j2se"); System.out.println("URL 为:" + url.toString()); System.out.println("协议为:" + url.getProtocol()); System.out.println("验证信息:" + url.getAuthority()); System.out.println("文件名及请求参数:" + url.getFile()); System.out.println("主机名:" + url.getHost()); System.out.println("路径:" + url.getPath()); System.out.println("端口:" + url.getPort()); System.out.println("默认端口:" + url.getDefaultPort()); System.out.println("请求参数:" + url.getQuery()); System.out.println("定位位置:" + url.getRef()); }catch(IOException e) { e.printStackTrace(); }{mtitle title="测试截图"/}
2022年07月06日
41 阅读
0 评论
0 点赞
2022-07-03
前端按钮级别权限判断
export function isAuth(key) { return JSON.parse(sessionStorage.getItem('permission') || ['']).indexOf(key) !== -1 || false; }在用户登录后,后端返回给前端的一个权限数组,例如['sys:user:add','pms:brand:add'],前端定义一个全局的isAuth()方法用于判断按钮是否有权限;
2022年07月03日
44 阅读
0 评论
0 点赞
2022-06-28
玩转 SpringBoot 监控统计(SQL监控、慢SQL记录、Spring监控、去广告)
基本概念添加依赖配置相关属性sql监控慢sql记录spring 监控去 Ad(广告)获取 Druid 的监控数据{mtitle title="基本概念"/}Druid 是Java语言中最好的数据库连接池。 虽然 HikariCP 的速度稍快,但是,Druid能够提供强大的监控和扩展功能 ,也是阿里巴巴的开源项目。 Druid是阿里巴巴开发的号称为监控而生的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource等等等,秒杀一切。 Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。 Spring Boot 默认数据源 HikariDataSource 与 JdbcTemplate中已经介绍 Spring Boot 2.x 默认使用 Hikari 数据源 ,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源。 而Druid已经在阿里巴巴部署了超过600个应用,经过好几年生产环境大规模部署的严苛考验!stat:Druid内置提供一个StatFilter,用于统计监控信息。wall:Druid防御SQL注入攻击的WallFilter就是通过Druid的SQL Parser分析。Druid提供的SQL Parser可以在JDBC层拦截SQL做相应处理,比如说分库分表、审计等。log4j2:这个就是 日志记录的功能,可以把sql语句打印到log4j2 供排查问题。{mtitle title="添加依赖"/}<!-- 阿里巴巴的druid数据源 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.23</version> </dependency> <!-- mysql8 驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--使用 log4j2 记录日志--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <!-- mybatis,引入了 SpringBoot的 JDBC 模块, 所以,默认是使用 hikari 作为数据源 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> <exclusions> <!-- 排除默认的 HikariCP 数据源 --> <exclusion> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </exclusion> </exclusions> </dependency>{mtitle title="配置相关属性"/}配置Druid数据源(连接池) :如同以前 c3p0、dbcp 数据源可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等一样,Druid 数据源同理可以进行设置;配置 Druid web 监控 filter(WebStatFilter) :这个过滤器的作用就是统计 web 应用请求中所有的数据库信息,比如 发出的 sql 语句,sql 执行的时间、请求次数、请求的 url 地址、以及seesion 监控、数据库表的访问次数 等等。配置 Druid 后台管理 Servlet(StatViewServlet) :Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面;需要设置 Druid 的后台管理页面的属性,比如 登录账号、密码 等;注意: Druid Spring Boot Starter 配置属性的名称完全遵照 Druid,可以通过 Spring Boot 配置文件来配置Druid数据库连接池和监控,如果没有配置则使用默认值。########## 配置数据源 (Druid)########## spring: datasource: ########## JDBC 基本配置 ########## username: xxx password: xxx driver-class-name: com.mysql.cj.jdbc.Driver # mysql8 的连接驱动 url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=Asia/Shanghai platform: mysql # 数据库类型 type: com.alibaba.druid.pool.DruidDataSource # 指定数据源类型 ########## 连接池 配置 ########## druid: # 配置初始化大小、最小、最大 initial-size: 5 minIdle: 10 max-active: 20 # 配置获取连接等待超时的时间(单位:毫秒) max-wait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 time-between-eviction-runs-millis: 2000 # 配置一个连接在池中最小生存的时间,单位是毫秒 min-evictable-idle-time-millis: 600000 max-evictable-idle-time-millis: 900000 # 用来测试连接是否可用的SQL语句,默认值每种数据库都不相同,这是mysql validationQuery: select 1 # 应用向连接池申请连接,并且testOnBorrow为false时,连接池将会判断连接是否处于空闲状态,如果是,则验证这条连接是否可用 testWhileIdle: true # 如果为true,默认是false,应用向连接池申请连接时,连接池会判断这条连接是否是可用的 testOnBorrow: false # 如果为true(默认false),当应用使用完连接,连接池回收连接的时候会判断该连接是否还可用 testOnReturn: false # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle poolPreparedStatements: true # 要启用PSCache,必须配置大于0,当大于0时, poolPreparedStatements自动触发修改为true, # 在Druid中,不会存在Oracle下PSCache占用内存过多的问题, # 可以把这个数值配置大一些,比如说100 maxOpenPreparedStatements: 20 # 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作 keepAlive: true # Spring 监控,利用aop 对指定接口的执行时间,jdbc数进行记录 aop-patterns: "com.springboot.template.dao.*" ########### 启用内置过滤器(第一个 stat必须,否则监控不到SQL)########## filters: stat,wall,log4j2 # 自己配置监控统计拦截的filter filter: # 开启druiddatasource的状态监控 stat: enabled: true db-type: mysql # 开启慢sql监控,超过2s 就认为是慢sql,记录到日志中 log-slow-sql: true slow-sql-millis: 2000 # 日志监控,使用slf4j 进行日志输出 slf4j: enabled: true statement-log-error-enabled: true statement-create-after-log-enabled: false statement-close-after-log-enabled: false result-set-open-after-log-enabled: false result-set-close-after-log-enabled: false ########## 配置WebStatFilter,用于采集web关联监控的数据 ########## web-stat-filter: enabled: true # 启动 StatFilter url-pattern: /* # 过滤所有url exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" # 排除一些不必要的url session-stat-enable: true # 开启session统计功能 session-stat-max-count: 1000 # session的最大个数,默认100 ########## 配置StatViewServlet(监控页面),用于展示Druid的统计信息 ########## stat-view-servlet: enabled: true # 启用StatViewServlet url-pattern: /druid/* # 访问内置监控页面的路径,内置监控页面的首页是/druid/index.html reset-enable: false # 不允许清空统计数据,重新计算 login-username: root # 配置监控页面访问密码 login-password: 123 allow: 127.0.0.1 # 允许访问的地址,如果allow没有配置或者为空,则允许所有访问 deny: # 拒绝访问的地址,deny优先于allow,如果在deny列表中,就算在allow列表中,也会被拒绝 上述配置文件的参数可以在 com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties 和 org.springframework.boot.autoconfigure.jdbc.DataSourceProperties中找到;{mtitle title="如何配置 Filter"/} 可以通过 spring.datasource.druid.filters=stat,wall,log4j ...的方式来启用相应的内置Filter,不过这些Filter都是默认配置。如果默认配置不能满足需求,可以放弃这种方式,通过配置文件来配置Filter,下面是例子。# 配置StatFilter spring.datasource.druid.filter.stat.enabled=true spring.datasource.druid.filter.stat.db-type=h2 spring.datasource.druid.filter.stat.log-slow-sql=true spring.datasource.druid.filter.stat.slow-sql-millis=2000 # 配置WallFilter spring.datasource.druid.filter.wall.enabled=true spring.datasource.druid.filter.wall.db-type=h2 spring.datasource.druid.filter.wall.config.delete-allow=false spring.datasource.druid.filter.wall.config.drop-table-allow=false目前为以下 Filter 提供了配置支持,根据(spring.datasource.druid.filter.*)进行配置。StatFilterWallFilterConfigFilterEncodingConvertFilterSlf4jLogFilterLog4jFilterLog4j2FilterCommonsLogFilter不想使用内置的 Filters,要想使自定义 Filter 配置生效需要将对应 Filter 的 enabled 设置为 true ,Druid Spring Boot Starter 默认禁用 StatFilter,可以将其 enabled 设置为 true 来启用它。{mtitle title="监控页面" /}启动项目后,访问 /druid/login.html 来到登录页面 ,输入用户名密码登录数据源页面 是当前DataSource配置的基本信息,上述配置的Filter可以在里面找到,如果没有配置Filter(一些信息会无法统计,例如“SQL监控”,会无法获取JDBC相关的SQL执行信息)SQL监控页面 ,统计了所有SQL语句的执行情况 URL监控页面 ,统计了所有Controller接口的访问以及执行情况Spring 监控页面,利用aop 对指定接口的执行时间,jdbc数进行记录SQL防火墙页面,druid提供了黑白名单的访问,可以清楚的看到sql防护情况。Session监控页面,可以看到当前的session状况,创建时间、最后活跃时间、请求次数、请求时间等详细参数】{mtitle title="sql监控"/} 配置 Druid web 监控 filter(WebStatFilter)这个过滤器,作用就是统计 web 应用请求中所有的数据库信息,比如 发出的 sql 语句,sql 执行的时间、请求次数、请求的 url 地址、以及seesion 监控、数据库表的访问次数 等等。spring: datasource: druid: ########## 配置WebStatFilter,用于采集web关联监控的数据 ########## web-stat-filter: enabled: true # 启动 StatFilter url-pattern: /* # 过滤所有url exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" # 排除一些不必要的url session-stat-enable: true # 开启session统计功能 session-stat-max-count: 1000 # session的最大个数,默认100{mtitle title="慢sql记录"/}有时候,系统中有些SQL执行很慢,我们希望使用日志记录下来,可以开启Druid的慢SQL记录功能spring: datasource: druid: filter: stat: enabled: true # 开启DruidDataSource状态监控 db-type: mysql # 数据库的类型 log-slow-sql: true # 开启慢SQL记录功能 slow-sql-millis: 2000 # 默认3000毫秒,这里超过2s,就是慢,记录到日志启动后,如果遇到执行慢的SQL,便会输出到日志中。{mtitle title="spring 监控"/}访问之后spring监控默认是没有数据的;这需要导入SprngBoot的AOP的Starter<!--SpringBoot 的aop 模块--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>需要在 application.yml 配置: spring监控AOP切入点,如com.springboot.template.dao.*,配置多个英文逗号分隔 spring.datasource.druid.aop-patterns="com.springboot.template.dao.*"{mtitle title="去 Ad(广告)"/} 访问监控页面的时候,你可能会在页面底部(footer)看到阿里巴巴的广告。原因:引入的druid的jar包中的common.js(里面有一段js代码是给页面的footer追加广告的),如果想去掉,有两种方式: 直接手动注释这段代码 如果是使用Maven,直接到本地仓库中,查找这个jar包 要注释的代码:// this.buildFooter();common.js的位置:com/alibaba/druid/1.1.23/druid-1.1.23.jar!/support/http/resources/js/common.js使用过滤器过滤: 注册一个过滤器,过滤common.js的请求,使用正则表达式替换相关的广告内容@Configuration @ConditionalOnWebApplication @AutoConfigureAfter(DruidDataSourceAutoConfigure.class) @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true", matchIfMissing = true) public class RemoveDruidAdConfig { /** * 方法名: removeDruidAdFilterRegistrationBean * 方法描述 除去页面底部的广告 * @param properties com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties * @return org.springframework.boot.web.servlet.FilterRegistrationBean */ @Bean public FilterRegistrationBean removeDruidAdFilterRegistrationBean(DruidStatProperties properties) { // 获取web监控页面的参数 DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); // 提取common.js的配置路径 String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); final String filePath = "support/http/resources/js/common.js"; //创建filter进行过滤 Filter filter = new Filter() { @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(request, response); // 重置缓冲区,响应头不会被重置 response.resetBuffer(); // 获取common.js String text = Utils.readFromResource(filePath); // 正则替换banner, 除去底部的广告信息 text = text.replaceAll("<a.*?banner\"></a><br/>", ""); text = text.replaceAll("powered.*?shrek.wang</a>", ""); response.getWriter().write(text); } @Override public void destroy() {} }; FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(filter); registrationBean.addUrlPatterns(commonJsPattern); return registrationBean; } }两种方式都可以,建议使用的是第一种,从根源解决。{mtitle title="获取 Druid 的监控数据"/} Druid 的监控数据可以在 开启 StatFilter 后 ,通过 DruidStatManagerFacade 进行获取; DruidStatManagerFacade#getDataSourceStatDataList 该方法可以获取所有数据源的监控数据, 除此之外 DruidStatManagerFacade 还提供了一些其他方法,可以按需选择使用。@RestController @RequestMapping(value = "/druid") public class DruidStatController { @GetMapping("/stat") public Object druidStat(){ // 获取数据源的监控数据 return DruidStatManagerFacade.getInstance().getDataSourceStatDataList(); } }
2022年06月28日
27 阅读
0 评论
0 点赞
2022-06-23
SerializerFeature.WriteDateUseDateFormat 避免序列化时,日期转为数字
为了避免在序列化的时候,将日期序列化程数字,可以使用:SerializerFeature.WriteDateUseDateFormat参数。PriceAgain.setPriceJson(JSON.toJSONString(BidderAginPriceList, SerializerFeature.WriteNullStringAsEmpty,SerializerFeature.WriteDateUseDateFormat));
2022年06月23日
31 阅读
0 评论
0 点赞
2022-06-17
RBAC 权限设计
权限设计客户端想访问资源接口时,都需要经过鉴权,才能正常访问,没有经过鉴权的请求, 都会被拒绝。采用 SpringSecurity OAuth2.0,对客户端进行授权及鉴权,步骤如下:客户端携带账号、密码,请求授权服务,授权服务验证账号、密码是否正确, 如果正确返回 token 令牌,否则告知客户端账号或密码不正确等。客户端携带 token 令牌请求资源接口,网关会请求鉴权服务,判断用户是否有权 限访问该资源,如果有权限,则返回资源的响应数据,否则告知客户端无权访问该资源。菜单权限系统分为超级管理员和普通管理员,超级管理员拥有所有功能权限及数据权限,主要职责是创建普通管理员账号,并给普通管理员分配权限。在系统里,拥有用户管理、 角色管理的账号,我们就可以理解成普通管理员,因为可以新建用户及分配权限。管理员新建用户、角色、部门时,会有一定的约束,保证不会有越权操作。数据权限数据权限分为全部数据权限、本部门及子部门数据权限、本部门数据权限、本人数据权限四种,根据登录的用户角色判断其权限范围,进行数据的过滤,没相应的数据权限,则不能查询、操作相应的数据。数据权限实现方式如下:用户登录后,获取其所属角色中权限最高的级别及所属部门。根据部门 ID 列表进行数据过滤,数据过滤表,都需要有部门 ID。数据库设计关系图表清单序号数据表名称1sys_dept部门表2sys_menu菜单权限表3sys_role角色信息表4sys_role_dept角色和部门关联表5sys_role_menu角色和菜单关联表6sys_user用户信息表7sys_user_role用户和角色关联表表字段明细1.1.1 sys_dept [组织架构表]#字段名称数据类型主键非空默认值备注说明1dept_id部门idBIGINT(20)√√ 2parent_id父部门idBIGINT(20) 0 3ancestors祖级列表VARCHAR(50) 4dept_name部门名称VARCHAR(30) 5order_num显示顺序INT(11) 0 6status部门状态CHAR(1) '0'0正常 1停用7del_flag删除标志CHAR(1) '0'0代表存在 2代表删除8create_by创建者VARCHAR(64) 9create_time创建时间DATETIME 10update_by更新者VARCHAR(64) 11update_time更新时间DATETIME 1.1.2 sys_menu [菜单权限表]#字段名称数据类型主键非空默认值备注说明1menu_id菜单IDBIGINT(20)√√ 2menu_name菜单名称VARCHAR(50) √ 3parent_id父菜单IDBIGINT(20) 0 4order_num显示顺序INT(11) 0 5path路由地址VARCHAR(200) 6component组件路径VARCHAR(255) 7menu_type菜单类型CHAR(1) M目录 C菜单 F按钮8visible菜单状态CHAR(1) '0'0显示 1隐藏9status菜单状态CHAR(1) '0'0正常 1停用10perms权限标识VARCHAR(100) 11icon菜单图标VARCHAR(100) '#' 12create_by创建者VARCHAR(64) 13create_time创建时间DATETIME 14update_by更新者VARCHAR(64) 15update_time更新时间DATETIME 16remark备注VARCHAR(500) 1.1.3 sys_role [角色信息表]#字段名称数据类型主键非空默认值备注说明1role_id角色IDBIGINT(20)√√ 2role_name角色名称VARCHAR(30) √ 3role_key角色权限字符串VARCHAR(100) √ 4role_sort显示顺序INT(11) √ 5data_scope数据范围CHAR(1) '1'1:全部数据权限 2:本部门及以下数据权限 3:本部门数据权限 4:本人权限6status角色状态CHAR(1) √ 0正常 1停用7del_flag删除标志CHAR(1) '0'0代表存在 2代表删除8create_by创建者VARCHAR(64) 9create_time创建时间DATETIME 10update_by更新者VARCHAR(64) 11update_time更新时间DATETIME 12remark备注VARCHAR(500) 1.1.4 sys_role_dept [角色和部门关联表]#字段名称数据类型主键非空默认值备注说明1role_id角色IDBIGINT(20)√√ 2dept_id部门IDBIGINT(20)√√ 1.1.5 sys_role_menu [角色和菜单关联表]#字段名称数据类型主键非空默认值备注说明1role_id角色IDBIGINT(20)√√ 2menu_id菜单IDBIGINT(20)√√ 1.1.6 sys_user [用户信息表]#字段名称数据类型主键非空默认值备注说明1user_id用户IDBIGINT(20)√√ 2dept_id部门IDBIGINT(20) 3user_name用户账号VARCHAR(30) √ 4nick_name用户昵称VARCHAR(30) √ 5user_type用户类型VARCHAR(2) '00'00 系统用户 01 B端用户 02 C端用户6email用户邮箱VARCHAR(50) 7phone_number手机号码VARCHAR(11) 8sex用户性别CHAR(1) '0'0男 1女 2未知9avatar头像地址VARCHAR(100) 10password密码VARCHAR(100) 11status帐号状态CHAR(1) '0'0正常 1停用12del_flag删除标志CHAR(1) '0'0代表存在 2代表删除13login_ip最后登录IPVARCHAR(128) 14login_date最后登录时间DATETIME 15create_by创建者VARCHAR(64) 16create_time创建时间DATETIME 17update_by更新者VARCHAR(64) 18update_time更新时间DATETIME 19remark备注VARCHAR(500) 1.1.7 sys_user_role [用户和角色关联表]#字段名称数据类型主键非空默认值备注说明1user_id用户IDBIGINT(20)√√ 2role_id角色IDBIGINT(20)√√ 建表SQLDROP TABLE IF EXISTS sys_dept; CREATE TABLE sys_dept( dept_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '部门id' , parent_id BIGINT(20) DEFAULT 0 COMMENT '父部门id' , ancestors VARCHAR(50) COMMENT '祖级列表' , dept_name VARCHAR(30) COMMENT '部门名称' , order_num INT(11) DEFAULT 0 COMMENT '显示顺序' , status CHAR(1) DEFAULT '0' COMMENT '部门状态;0正常 1停用' , del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志;0代表存在 2代表删除' , create_by VARCHAR(64) COMMENT '创建者' , create_time DATETIME COMMENT '创建时间' , update_by VARCHAR(64) COMMENT '更新者' , update_time DATETIME COMMENT '更新时间' , PRIMARY KEY (dept_id) ) COMMENT = '组织架构表'; DROP TABLE IF EXISTS sys_menu; CREATE TABLE sys_menu( menu_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID' , menu_name VARCHAR(50) NOT NULL COMMENT '菜单名称' , parent_id BIGINT(20) DEFAULT 0 COMMENT '父菜单ID' , order_num INT(11) DEFAULT 0 COMMENT '显示顺序' , path VARCHAR(200) COMMENT '路由地址' , component VARCHAR(255) COMMENT '组件路径' , menu_type CHAR(1) COMMENT '菜单类型;M目录 C菜单 F按钮' , visible CHAR(1) DEFAULT '0' COMMENT '菜单状态;0显示 1隐藏' , status CHAR(1) DEFAULT '0' COMMENT '菜单状态;0正常 1停用' , perms VARCHAR(100) COMMENT '权限标识' , icon VARCHAR(100) DEFAULT '#' COMMENT '菜单图标' , create_by VARCHAR(64) COMMENT '创建者' , create_time DATETIME COMMENT '创建时间' , update_by VARCHAR(64) COMMENT '更新者' , update_time DATETIME COMMENT '更新时间' , remark VARCHAR(500) COMMENT '备注' , PRIMARY KEY (menu_id) ) COMMENT = '菜单权限表'; DROP TABLE IF EXISTS sys_role; CREATE TABLE sys_role( role_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID' , role_name VARCHAR(30) NOT NULL COMMENT '角色名称' , role_key VARCHAR(100) NOT NULL COMMENT '角色权限字符串' , role_sort INT(11) NOT NULL COMMENT '显示顺序' , data_scope CHAR(1) DEFAULT '1' COMMENT '数据范围;1:全部数据权限 2:本部门及以下数据权限 3:本部门数据权限 4:本人权限' , status CHAR(1) NOT NULL COMMENT '角色状态;0正常 1停用' , del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志;0代表存在 2代表删除' , create_by VARCHAR(64) COMMENT '创建者' , create_time DATETIME COMMENT '创建时间' , update_by VARCHAR(64) COMMENT '更新者' , update_time DATETIME COMMENT '更新时间' , remark VARCHAR(500) COMMENT '备注' , PRIMARY KEY (role_id) ) COMMENT = '角色信息表'; DROP TABLE IF EXISTS sys_role_dept; CREATE TABLE sys_role_dept( role_id BIGINT(20) NOT NULL COMMENT '角色ID' , dept_id BIGINT(20) NOT NULL COMMENT '部门ID' , PRIMARY KEY (role_id,dept_id) ) COMMENT = '角色和部门关联表'; DROP TABLE IF EXISTS sys_role_menu; CREATE TABLE sys_role_menu( role_id BIGINT(20) NOT NULL COMMENT '角色ID' , menu_id BIGINT(20) NOT NULL COMMENT '菜单ID' , PRIMARY KEY (role_id,menu_id) ) COMMENT = '角色和菜单关联表'; DROP TABLE IF EXISTS sys_user; CREATE TABLE sys_user( user_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID' , dept_id BIGINT(20) COMMENT '部门ID' , user_name VARCHAR(30) NOT NULL COMMENT '用户账号' , nick_name VARCHAR(30) NOT NULL COMMENT '用户昵称' , user_type VARCHAR(2) DEFAULT '00' COMMENT '用户类型;00系统用户 01:B端用户 02:C端用户' , email VARCHAR(50) COMMENT '用户邮箱' , phone_number VARCHAR(11) COMMENT '手机号码' , sex CHAR(1) DEFAULT '0' COMMENT '用户性别;0男 1女 2未知' , avatar VARCHAR(100) COMMENT '头像地址' , password VARCHAR(100) COMMENT '密码' , status CHAR(1) DEFAULT '0' COMMENT '帐号状态;0正常 1停用' , del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志;0代表存在 2代表删除' , login_ip VARCHAR(128) COMMENT '最后登录IP' , login_date DATETIME COMMENT '最后登录时间' , create_by VARCHAR(64) COMMENT '创建者' , create_time DATETIME COMMENT '创建时间' , update_by VARCHAR(64) COMMENT '更新者' , update_time DATETIME COMMENT '更新时间' , remark VARCHAR(500) COMMENT '备注' , PRIMARY KEY (user_id) ) COMMENT = '用户信息表'; DROP TABLE IF EXISTS sys_user_role; CREATE TABLE sys_user_role( user_id BIGINT(20) NOT NULL COMMENT '用户ID' , role_id BIGINT(20) NOT NULL COMMENT '角色ID' , PRIMARY KEY (user_id,role_id) ) COMMENT = '用户和角色关联表';
2022年06月17日
105 阅读
0 评论
0 点赞
2022-05-29
几行代码搞定 SpringBoot 接口恶意刷新和暴力请求
在实际项目使用中,必须要考虑服务的安全性,当服务部署到互联网以后,就要考虑服务被恶意请求和暴力攻击的情况,下面的教程,通过intercept和redis针对url+ip在一定时间内访问的次数来将ip禁用,可以根据自己的需求进行相应的修改,来达到自己的目的。一:创建一个自定义的拦截器类,也是最核心的代码:@Slf4j public class IpUrlLimitInterceptor implements HandlerInterceptor { private RedisUtil getRedisUtil() { return SpringContextUtil.getBean(RedisUtil.class); } private static final String LOCK_IP_URL_KEY="lock_ip_"; private static final String IP_URL_REQ_TIME="ip_url_times_"; private static final long LIMIT_TIMES=5; private static final int IP_LOCK_TIME=60; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { log.info("request请求地址uri={},ip={}", httpServletRequest.getRequestURI(), IpAdrressUtil.getIpAdrress(httpServletRequest)); if (ipIsLock(IpAdrressUtil.getIpAdrress(httpServletRequest))){ log.info("ip访问被禁止={}",IpAdrressUtil.getIpAdrress(httpServletRequest)); ApiResult result = new ApiResult(ResultEnum.LOCK_IP); returnJson(httpServletResponse, JSON.toJSONString(result)); return false; } if(!addRequestTime(IpAdrressUtil.getIpAdrress(httpServletRequest),httpServletRequest.getRequestURI())){ ApiResult result = new ApiResult(ResultEnum.LOCK_IP); returnJson(httpServletResponse, JSON.toJSONString(result)); return false; } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } /** * @Description: 判断ip是否被禁用 * @date: 2019-10-12 13:08 * @param ip * @return java.lang.Boolean */ private Boolean ipIsLock(String ip){ RedisUtil redisUtil=getRedisUtil(); if(redisUtil.hasKey(LOCK_IP_URL_KEY+ip)){ return true; } return false; } /** * @Description: 记录请求次数 * @date: 2019-10-12 17:18 * @param ip * @param uri * @return java.lang.Boolean */ private Boolean addRequestTime(String ip,String uri){ String key=IP_URL_REQ_TIME+ip+uri; RedisUtil redisUtil=getRedisUtil(); if (redisUtil.hasKey(key)){ long time=redisUtil.incr(key,(long)1); if (time>=LIMIT_TIMES){ redisUtil.getLock(LOCK_IP_URL_KEY+ip,ip,IP_LOCK_TIME); return false; } }else { redisUtil.getLock(key,(long)1,1); } return true; } private void returnJson(HttpServletResponse response, String json) throws Exception { PrintWriter writer = null; response.setCharacterEncoding("UTF-8"); response.setContentType("text/json; charset=utf-8"); try { writer = response.getWriter(); writer.print(json); } catch (IOException e) { log.error("LoginInterceptor response error ---> {}", e.getMessage(), e); } finally { if (writer != null) { writer.close(); } } } } 代码中redis的使用的是分布式锁的形式,这样可以最大程度保证线程安全和功能的实现效果。代码中设置的是1S内同一个接口通过同一个ip访问5次,就将该ip禁用1个小时,根据自己项目需求可以自己适当修改,实现自己想要的功能:二、redis分布式锁的关键代码:/** * @className: RedisUtil * @since: 0.1 **/ @Component @Slf4j public class RedisUtil { private static final Long SUCCESS = 1L; @Autowired private RedisTemplate<String, Object> redisTemplate; // =============================common============================ /** * 获取锁 * @param lockKey * @param value * @param expireTime:单位-秒 * @return */ public boolean getLock(String lockKey, Object value, int expireTime) { try { log.info("添加分布式锁key={},expireTime={}",lockKey,expireTime); String script = "if redis.call('setNx',KEYS[1],ARGV[1]) then if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end end"; RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class); Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value, expireTime); if (SUCCESS.equals(result)) { return true; } } catch (Exception e) { e.printStackTrace(); } return false; } /** * 释放锁 * @param lockKey * @param value * @return */ public boolean releaseLock(String lockKey, String value) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class); Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value); if (SUCCESS.equals(result)) { return true; } return false; } }三、最后将上面自定义的拦截器通过registry.addInterceptor添加一下,就生效了;@Configuration @Slf4j public class MyWebAppConfig extends WebMvcConfigurerAdapter { @Bean IpUrlLimitInterceptor getIpUrlLimitInterceptor(){ return new IpUrlLimitInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(getIpUrlLimitInterceptor()).addPathPatterns("/**"); super.addInterceptors(registry); } }
2022年05月29日
42 阅读
0 评论
0 点赞
2022-05-14
element-UI 合并单元格
<el-table :data="dataList" border :span-method="spanMethod" size="mini" style="width:100%" > <el-table-column prop="name" label="姓名" align="center" show-overflow-tooltip width="200" ></el-table-column> <el-table-column prop="subjectName" label="学科" align="center" width="120" > </el-table-column> <el-table-column prop="subjectScore" label="成绩" align="center" width="120" > </el-table-column> </el-table> watch: { dataList (val) { this.getSpanArr(val) } }, created () { this.getDataList() }, // 获取数据 getDataList () { this.dataList = [ { id: '1', name: '张三', subjectName: '语文', subjectScore: 65 }, { id: '1', name: '张三', subjectName: '数学', subjectScore: 71 }, { id: '1', name: '张三', subjectName: '英语', subjectScore: 82 }, { id: '2', name: '李四', subjectName: '语文', subjectScore: 60 }, { id: '2', name: '李四', subjectName: '数学', subjectScore: 88 } ] }, // 合并单元格 getSpanArr (data) { this.spanArr = [] if (data == null) { return } for (var i = 0; i < data.length; i++) { if (i === 0) { this.spanArr.push(1) this.pos = 0 } else { // 判断当前元素与上一个元素是否相同 if (data[i].id === data[i - 1].id) { this.spanArr[this.pos] += 1 this.spanArr.push(0) } else { this.spanArr.push(1) this.pos = i } } } }, // 合并单元格 spanMethod ({ row, column, rowIndex, columnIndex }) { if (columnIndex < 1) { const _row = this.spanArr[rowIndex] const _col = _row > 0 ? 1 : 0 return { rowspan: _row, colspan: _col } } },
2022年05月14日
67 阅读
0 评论
0 点赞
2022-05-03
项目中常用的19条MySQL优化
一、EXPLAIN 做MySQL优化,我们要善用 EXPLAIN 查看SQL执行计划。下面来个简单的示例,标注(1,2,3,4,5)我们要重点关注的数据type列,连接类型。一个好的sql语句至少要达到range级别。杜绝出现all级别key列,使用到的索引名。如果没有选择索引,值是NULL。可以采取强制索引方式key_len列,索引长度rows列,扫描行数。该值是个预估值extra列,详细说明。注意常见的不太友好的值有:Using filesort, Using temporary二、SQL语句中IN包含的值不应过多 MySQL对于IN做了相应的优化,即将IN中的常量全部存储在一个数组里面,而且这个数组是排好序的。但是如果数值较多,产生的消耗也是比较大的。再例如:select id from table_name where num in(1,2,3) 对于连续的数值,能用 between 就不要用 in 了;再或者使用连接来替换。三、SELECT语句务必指明字段名称 SELECT *增加很多不必要的消耗(cpu、io、内存、网络带宽);增加了使用覆盖索引的可能性;当表结构发生改变时,前端也需要更新。所以要求直接在select后面接上字段名。四、当只需要一条数据的时候,使用limit 1 这是为了使EXPLAIN中type列达到const类型五、如果排序字段没有用到索引,就尽量少排序六、如果限制条件中其他字段没有索引,尽量少用or or两边的字段中,如果有一个不是索引字段,而其他条件也不是索引字段,会造成该查询不走索引的情况。很多时候使用 union all 或者是union(必要的时候)的方式来代替“or”会得到更好的效果。七、尽量用union all代替union union和union all的差异主要是前者需要将结果集合并后再进行唯一性过滤操作,这就会涉及到排序,增加大量的CPU运算,加大资源消耗及延迟。当然,union all的前提条件是两个结果集没有重复数据。八、不使用ORDER BY RAND()select id from `table_name` order by rand() limit 1000;上面的sql语句,可优化为select id from `table_name` t1 join (select rand() * (select max(id) from `table_name`) as nid) t2 on t1.id > t2.nid limit 1000;九、区分in和exists, not in和not existsselect * from 表A where id in (select id from 表B)上面sql语句相当于select * from 表A where exists(select * from 表B where 表B.id=表A.id) 区分in和exists主要是造成了驱动顺序的改变(这是性能变化的关键),如果是exists,那么以外层表为驱动表,先被访问,如果是IN,那么先执行子查询。所以 IN适合于外表大而内表小的情况;EXISTS适合于外表小而内表大的情况 。 关于not in和not exists,推荐使用not exists,不仅仅是效率问题,not in可能存在逻辑问题。如何高效的写出一个替代not exists的sql语句? 原sql语句:select colname … from A表 where a.id not in (select b.id from B表)高效的sql语句:select colname … from A表 Left join B表 on where a.id = b.id where b.id is null取出的结果集如下图表示,A表不在B表中的数据:十、使用合理的分页方式以提高分页的效率select id,name from table_name limit 866613, 20 使用上述sql语句做分页的时候,可能有人会发现,随着表数据量的增加,直接使用limit分页查询会越来越慢。优化的方法如下:可以取前一页的最大行数的id,然后根据这个最大的id来限制下一页的起点。比如此列中,上一页最大的id是866612。sql可以采用如下的写法:select id,name from table_name where id> 866612 limit 20十一、分段查询 在一些用户选择页面中,可能一些用户选择的时间范围过大,造成查询缓慢。主要的原因是扫描行数过多。这个时候可以通过程序,分段进行查询,循环遍历,将结果合并处理进行展示。如下图这个sql语句,扫描的行数成百万级以上的时候就可以使用分段查询:十二、避免在 where 子句中对字段进行 null 值判断对于null的判断会导致引擎放弃使用索引而进行全表扫描。十三、不建议使用%前缀模糊查询 例如LIKE “%name”或者LIKE “%name%”,这种查询会导致索引失效而进行全表扫描。但是可以使用LIKE “name%”。 那如何查询%name%? 如下图所示,虽然给secret字段添加了索引,但在explain结果果并没有使用。 那么如何解决这个问题呢,答案:使用全文索引。 在我们查询中经常会用到:select id,fnum,fdst from table_name where user_name like '%zhangsan%'; 这样的语句,普通索引是无法满足查询需求的。庆幸的是在MySQL中,有全文索引来帮助我们。创建全文索引的sql语法是:select id,fnum,fdst from table_name where user_name like '%zhangsan%'; 使用全文索引的sql语句是:select id,fnum,fdst from table_name where match(user_name) against('zhangsan' in boolean mode);注意:在需要创建全文索引之前,请联系DBA确定能否创建。同时需要注意的是查询语句的写法与普通索引的区别。十四、避免在where子句中对字段进行表达式操作比如:select user_id,user_project from table_name where age*2=36;中对字段就行了算术运算,这会造成引擎放弃使用索引,建议改成:select user_id,user_project from table_name where age=36/2;十五、避免隐式类型转换 where 子句中出现 column 字段的类型和传入的参数类型不一致的时候发生的类型转换,建议先确定where中的参数类型十六、对于联合索引来说,要遵守最左前缀法则 举列来说索引含有字段id,name,school,可以直接用id字段,也可以id,name这样的顺序,但是name;school都无法使用这个索引。所以在创建联合索引的时候一定要注意索引字段顺序,常用的查询字段放在最前面。十七、必要时可以使用force index来强制查询走某个索引 有的时候MySQL优化器采取它认为合适的索引来检索sql语句,但是可能它所采用的索引并不是我们想要的。这时就可以采用force index来强制优化器使用我们制定的索引。十八、注意范围查询语句 对于联合索引来说,如果存在范围查询,比如between,>,<等条件时,会造成后面的索引字段失效。十九、关于JOIN优化LEFT JOIN A表为驱动表INNER JOIN MySQL会自动找出那个数据少的表作用驱动表RIGHT JOIN B表为驱动表注意:MySQL中没有full join,可以用以下方式来解决select * from A left join B on B.name = A.name where B.name is null union all select * from B;尽量使用inner join,避免left join 参与联合查询的表至少为2张表,一般都存在大小之分。如果连接方式是inner join,在没有其他过滤条件的情况下MySQL会自动选择小表作为驱动表,但是left join在驱动表的选择上遵循的是左边驱动右边的原则,即left join左边的表名为驱动表。 合理利用索引 被驱动表的索引字段作为on的限制字段。 利用小表去驱动大表 从原理图能够直观的看出如果能够减少驱动表的话,减少嵌套循环中的循环次数,以减少 IO总量及CPU运算的次数。 巧用STRAIGHT_JOIN inner join是由mysql选择驱动表,但是有些特殊情况需要选择另个表作为驱动表,比如有group by、order by等「Using filesort」、「Using temporary」时。STRAIGHT_JOIN来强制连接顺序,在STRAIGHT_JOIN左边的表名就是驱动表,右边则是被驱动表。在使用STRAIGHT_JOIN有个前提条件是该查询是内连接,也就是inner join。其他链接不推荐使用STRAIGHT_JOIN,否则可能造成查询结果不准确。这个方式有时可能减少3倍的时间。
2022年05月03日
46 阅读
0 评论
0 点赞
2022-05-02
使用Prometheus和Grafana搭建SpringBoot应用监控系统
推荐阅读https://www.cnblogs.com/throwable/p/13257557.htmlhttps://zhuanlan.zhihu.com/p/273229856https://prometheus.io/docs/prometheus/latest/getting_started/最近公司项目介绍的时候看到了类似监控系统的展示页面,比如资源利用、GC次数、Kafka生产消费等,清晰明了,页面十分酷炫。{mtitle title="Grafana"/}简介 Grafana是一款Go语言开发的开源数据可视化工具,可以做数据监控和数据统计,还提供了很多“仪表板”,可以实现很多炫酷且实用的可视化指标。技术点 Spring boot actuator/micrometer(收集)、Prometheus(存储聚合)、Grafana(可视化)核心思路 以spring boot项目为例,Spring Boot Actuator提供了很多监控指标,可以通过RestFul的形式访问查看,但原始的为json数据,而非上图展示的页面。并且只是瞬时值,不能提供段时间内数据的的聚合分析等。当然这些也可以自行通过数据持久化,并开发前端页面的方式实现。而通过了解,目前已有这样的轮子,就是Prometheus,内部实现时序数据库,可以将收集到的监控数据存储,并且可以结合Grafana进行数据可视化,目前Prometheus已经成为热门且通用的监控解决方案。 关键组件之Exporter:作用类似转换器,各种被监控的对象可以基于共同的Prometheus提供的规范进行实现,从而让自己都能够接入到Prometheus。添加依赖 添加actuator是支持输出监控信息,micrometer-registry-prometheus是能够把actuator监控信息,转化为prometheus能够处理的格式。<!--添加prometheus和actuator依赖--> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>修改application.yml配置文件 在配置文件中,配置endpoint暴露Prometheus,并允许将指标metrics导入到Prometheus中。server: port: 10010 spring: application: # 应用名,后续会以此展示 name: Spring Boot Monitor # 监控相关配置 # 开启监控 management: endpoints: web: exposure: include: "*" # 端点 endpoint: prometheus: enabled: true health: show-details: always # 指标 metrics: export: prometheus: enabled: true添加指定的应用名可以直接通过配置文件引用metrics: tags: application: ${spring.application.name}也可通过启动类,注入import io.micrometer.core.instrument.MeterRegistry; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class SpringBootMonitorApplication { public static void main(String[] args) { SpringApplication.run(SpringBootMonitorApplication.class, args); } /** * 注册应用名,后续监控中展示该应用名 * * @param applicationName 取配置文件中的应用名 */ @Bean MeterRegistryCustomizer<MeterRegistry> configure(@Value("${spring.application.name}") String applicationName) { return (registry -> registry.config().commonTags("application", applicationName)); } }启动项目访问http://localhost:10010/actuator,已经可以得到监控信息,并且包含prometheus的可以访问访问http://localhost:10010/actuator/prometheus,也可以得到监控信息(prometheus格式的)配置Prometheus下载镜像:docker pull prom/prometheus配置文件: 创建本地用于挂载的数据卷目录/mydata/prometheus,并新建配置文件。global: scrape_interval: 15s # By default, scrape targets every 15 seconds. # Attach these labels to any time series or alerts when communicating with # external systems (federation, remote storage, Alertmanager). external_labels: monitor: 'codelab-monitor' # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. scrape_configs: # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config. - job_name: 'prometheus' # Override the global default and scrape targets from this job every 5 seconds. scrape_interval: 5s static_configs: - targets: ['localhost:9090'] # 额外添加的任务配置,每隔五秒钟抓取数据 - job_name: 'springboot-prometheus' scrape_interval: 5s metrics_path: '/actuator/prometheus' static_configs: # spring boot服务运行的服务器地址。我是物理机运行的,填写的是本机地址 - targets: ['192.168.3.47:10010'] 启动容器:docker run -d --name=prometheus \ -p 9090:9090 \ -v /mydata/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \ -d prom/prometheus 浏览器访问prometheus默认地址http://172.16.224.100:9090,可以正常访问首页,http://172.16.224.100:9090/metrics可以获取到数据。 选择【 status 】->【 targets 】,即可看到之前配置文件中配置的springboot信息。配置Grafana下载镜像:docker pull grafana/grafana启动容器:docker run -d --name=grafana -p 3000:3000 grafana/grafana访问页面: 默认端口是3000,默认用户名密码都是admin 添加prometheus数据源 添加数据源,选择prometheus,并配置URL点击保存按钮,成功会提示选择合适的仪表盘 官方提供了很多https://grafana.com/grafana/dashboards/ 如,Grafana提供的JVM面板,记住右侧的编码。在自己的Grafana页面导入该编码即可。 导入编码 选择数据源prometheus,导入完成后即可看到效果。
2022年05月02日
92 阅读
0 评论
0 点赞
2022-05-02
SpringBoot + SpringBatch + Quartz整合定时批量任务
一、引入依赖<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> </dependencies>二、修改application.yml 文件配置三、Service实现类,触发批处理任务的入口,执行一个job@Service("batchService") public class BatchServiceImpl implements BatchService { // 框架自动注入 @Autowired private JobLauncher jobLauncher; @Autowired private Job updateDeviceJob; /** * 根据 taskId 创建一个Job * @param taskId * @throws Exception */ @Override public void createBatchJob(String taskId) throws Exception { JobParameters jobParameters = new JobParametersBuilder() .addString("taskId", taskId) .addString("uuid", UUID.randomUUID().toString().replace("-","")) .toJobParameters(); // 传入一个Job任务和任务需要的参数 jobLauncher.run(updateDeviceJob, jobParameters); } }四、SpringBatch配置类,此部分最重要@Configuration public class BatchConfiguration { private static final Logger log = LoggerFactory.getLogger(BatchConfiguration.class); @Value("${batch-size:5000}") private int batchSize; // 框架自动注入 @Autowired public JobBuilderFactory jobBuilderFactory; // 框架自动注入 @Autowired public StepBuilderFactory stepBuilderFactory; // 数据过滤器,对从数据库读出来的数据,注意进行操作 @Autowired public TaskItemProcessor taskItemProcessor; // 接收job参数 public Map<String, JobParameter> parameters; public Object taskId; @Autowired private JdbcTemplate jdbcTemplate; // 读取数据库操作 @Bean @StepScope public JdbcCursorItemReader<DispatchRequest> itemReader(DataSource dataSource) { String querySql = " SELECT " + " e. ID AS taskId, " + " e.user_id AS userId, " + " e.timing_startup AS startTime, " + " u.device_id AS deviceId, " + " d.app_name AS appName, " + " d.compose_file AS composeFile, " + " e.failure_retry AS failureRetry, " + " e.tetry_times AS retryTimes, " + " e.device_managered AS deviceManagered " + " FROM " + " eiot_upgrade_task e " + " LEFT JOIN eiot_upgrade_device u ON e. ID = u.upgrade_task_id " + " LEFT JOIN eiot_app_detail d ON e.app_id = d. ID " + " WHERE " + " ( " + " u.device_upgrade_status = 0 " + " OR u.device_upgrade_status = 2" + " )" + " AND e.tetry_times > u.retry_times " + " AND e. ID = ?"; return new JdbcCursorItemReaderBuilder<DispatchRequest>() .name("itemReader") .sql(querySql) .dataSource(dataSource) .queryArguments(new Object[]{parameters.get("taskId").getValue()}) .rowMapper(new DispatchRequest.DispatchRequestRowMapper()) .build(); } // 将结果写回数据库 @Bean @StepScope public ItemWriter<ProcessResult> itemWriter() { return new ItemWriter<ProcessResult>() { private int updateTaskStatus(DispatchRequest dispatchRequest, int status) { log.info("update taskId: {}, deviceId: {} to status {}", dispatchRequest.getTaskId(), dispatchRequest.getDeviceId(), status); Integer retryTimes = jdbcTemplate.queryForObject( "select retry_times from eiot_upgrade_device where device_id = ? and upgrade_task_id = ?", new Object[]{ dispatchRequest.getDeviceId(), dispatchRequest.getTaskId()}, Integer.class ); retryTimes += 1; int updateCount = jdbcTemplate.update("update eiot_upgrade_device set device_upgrade_status = ?, retry_times = ? " + "where device_id = ? and upgrade_task_id = ?", status, retryTimes, dispatchRequest.getDeviceId(), dispatchRequest.getTaskId()); if (updateCount <= 0) { log.warn("no task updated"); } else { log.info("count of {} task updated", updateCount); } // 最后一次重试 if (status == STATUS_DISPATCH_FAILED && retryTimes == dispatchRequest.getRetryTimes()) { log.info("the last retry of {} failed, inc deviceManagered", dispatchRequest.getTaskId()); return 1; } else { return 0; } } @Override @Transactional public void write(List<? extends ProcessResult> list) throws Exception { Map taskMap = jdbcTemplate.queryForMap( "select device_managered, device_count, task_status from eiot_upgrade_task where id = ?", list.get(0).getDispatchRequest().getTaskId() // 我们认定一个批量里面,taskId都是一样的 ); int deviceManagered = (int)taskMap.get("device_managered"); Integer deviceCount = (Integer) taskMap.get("device_count"); if (deviceCount == null) { log.warn("deviceCount of task {} is null", list.get(0).getDispatchRequest().getTaskId()); } int taskStatus = (int)taskMap.get("task_status"); for (ProcessResult result: list) { deviceManagered += updateTaskStatus(result.getDispatchRequest(), result.getStatus()); } if (deviceCount != null && deviceManagered == deviceCount) { taskStatus = 2; //任务状态 0:待升级,1:升级中,2:已完成 } jdbcTemplate.update("update eiot_upgrade_task set device_managered = ?, task_status = ? " + "where id = ?", deviceManagered, taskStatus, list.get(0).getDispatchRequest().getTaskId()); } }; } /** * 定义一个下发更新的 job * @return */ @Bean public Job updateDeviceJob(Step updateDeviceStep) { return jobBuilderFactory.get(UUID.randomUUID().toString().replace("-", "")) .listener(new JobListener()) // 设置Job的监听器 .flow(updateDeviceStep)// 执行下发更新的Step .end() .build(); } /** * 定义一个下发更新的 step * @return */ @Bean public Step updateDeviceStep(JdbcCursorItemReader<DispatchRequest> itemReader,ItemWriter<ProcessResult> itemWriter) { return stepBuilderFactory.get(UUID.randomUUID().toString().replace("-", "")) .<DispatchRequest, ProcessResult> chunk(batchSize) .reader(itemReader) //根据taskId从数据库读取更新设备信息 .processor(taskItemProcessor) // 每条更新信息,执行下发更新接口 .writer(itemWriter) .build(); } // job 监听器 public class JobListener implements JobExecutionListener { @Override public void beforeJob(JobExecution jobExecution) { log.info(jobExecution.getJobInstance().getJobName() + " before... "); parameters = jobExecution.getJobParameters().getParameters(); taskId = parameters.get("taskId").getValue(); log.info("job param taskId : " + parameters.get("taskId")); } @Override public void afterJob(JobExecution jobExecution) { log.info(jobExecution.getJobInstance().getJobName() + " after... "); // 当所有job执行完之后,查询设备更新状态,如果有失败,则要定时重新执行job String sql = " SELECT " + " count(*) " + " FROM " + " eiot_upgrade_device d " + " LEFT JOIN eiot_upgrade_task u ON d.upgrade_task_id = u. ID " + " WHERE " + " u. ID = ? " + " AND d.retry_times < u.tetry_times " + " AND ( " + " d.device_upgrade_status = 0 " + " OR d.device_upgrade_status = 2 " + " ) "; // 获取更新失败的设备个数 Integer count = jdbcTemplate.queryForObject(sql, new Object[]{taskId}, Integer.class); log.info("update device failure count : " + count); // 下面是使用Quartz触发定时任务 // 获取任务时间,单位秒 // String time = jdbcTemplate.queryForObject(sql, new Object[]{taskId}, Integer.class); // 此处方便测试,应该从数据库中取taskId对应的重试间隔,单位秒 Integer millSecond = 10; if(count != null && count > 0){ String jobName = "UpgradeTask_" + taskId; String reTaskId = taskId.toString(); Map<String,Object> params = new HashMap<>(); params.put("jobName",jobName); params.put("taskId",reTaskId); if (QuartzManager.checkNameNotExist(jobName)) { QuartzManager.scheduleRunOnceJob(jobName, RunOnceJobLogic.class,params,millSecond); } } } } }Processor,处理每条数据,可以在此对数据进行过滤操作。@Component("taskItemProcessor") public class TaskItemProcessor implements ItemProcessor<DispatchRequest, ProcessResult> { public static final int STATUS_DISPATCH_FAILED = 2; public static final int STATUS_DISPATCH_SUCC = 1; private static final Logger log = LoggerFactory.getLogger(TaskItemProcessor.class); @Value("${upgrade-dispatch-base-url:http://localhost/api/v2/rpc/dispatch/command/}") private String dispatchUrl; @Autowired JdbcTemplate jdbcTemplate; /** * 在这里,执行 下发更新指令 的操作 * @param dispatchRequest * @return * @throws Exception */ @Override public ProcessResult process(final DispatchRequest dispatchRequest) { // 调用接口,下发指令 String url = dispatchUrl + dispatchRequest.getDeviceId()+"/"+dispatchRequest.getUserId(); log.info("request url:" + url); RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON_UTF8); MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>(); JSONObject jsonOuter = new JSONObject(); JSONObject jsonInner = new JSONObject(); try { jsonInner.put("jobId",dispatchRequest.getTaskId()); jsonInner.put("name",dispatchRequest.getName()); jsonInner.put("composeFile", Base64Util.bytesToBase64Str(dispatchRequest.getComposeFile())); jsonInner.put("policy",new JSONObject().put("startTime",dispatchRequest.getPolicy())); jsonInner.put("timestamp",dispatchRequest.getTimestamp()); jsonOuter.put("method","updateApp"); jsonOuter.put("params",jsonInner); } catch (JSONException e) { log.info("JSON convert Exception :" + e); }catch (IOException e) { log.info("Base64Util bytesToBase64Str :" + e); } log.info("request body json :" + jsonOuter); HttpEntity<String> requestEntity = new HttpEntity<String>(jsonOuter.toString(),headers); int status; try { ResponseEntity<String> response = restTemplate.postForEntity(url,requestEntity,String.class); log.info("response :" + response); if (response.getStatusCode() == HttpStatus.OK) { status = STATUS_DISPATCH_SUCC; } else { status = STATUS_DISPATCH_FAILED; } }catch (Exception e){ status = STATUS_DISPATCH_FAILED; } return new ProcessResult(dispatchRequest, status); } }
2022年05月02日
63 阅读
0 评论
0 点赞
2022-05-01
2万字详解!详解 Java8 Stream 用法,从此告别shi山(垃圾代码)
Java8的新特性主要是Lambda表达式和流,当流和Lambda表达式结合起来一起使用时,因为流申明式处理数据集合的特点,可以让代码变得简洁易读。菜肴:Dish.javapublic class Dish { private String name; private boolean vegetarian; private int calories; private Type type; // getter and setter }{mtitle title="筛选和排序"/}如果有一个需求,需要对数据库查询到的菜肴进行一个处理:筛选出卡路里小于400的菜肴对筛选出的菜肴进行一个排序获取排序后菜肴的名字Java8以前的实现方式private List<String> beforeJava7(List<Dish> dishList) { List<Dish> lowCaloricDishes = new ArrayList<>(); //1.筛选出卡路里小于400的菜肴 for (Dish dish : dishList) { if (dish.getCalories() < 400) { lowCaloricDishes.add(dish); } } //2.对筛选出的菜肴进行排序 Collections.sort(lowCaloricDishes, new Comparator<Dish>() { @Override public int compare(Dish o1, Dish o2) { return Integer.compare(o1.getCalories(), o2.getCalories()); } }); //3.获取排序后菜肴的名字 List<String> lowCaloricDishesName = new ArrayList<>(); for (Dish d : lowCaloricDishes) { lowCaloricDishesName.add(d.getName()); } return lowCaloricDishesName; }Java8之后的实现方式private List<String> afterJava8(List<Dish> dishList) { return dishList.stream() .filter(d -> d.getCalories() < 400) //筛选出卡路里小于400的菜肴 .sorted(comparing(Dish::getCalories)) //根据卡路里进行排序 .map(Dish::getName) //提取菜肴名称 .collect(Collectors.toList()); //转换为List }{mtitle title="分组"/}对数据库查询到的菜肴根据菜肴种类进行分类,返回一个Map<Type, List>的结果Java8以前的实现方式private static Map<Type, List<Dish>> beforeJdk8(List<Dish> dishList) { Map<Type, List<Dish>> result = new HashMap<>(); for (Dish dish : dishList) { //不存在则初始化 if (result.get(dish.getType())==null) { List<Dish> dishes = new ArrayList<>(); dishes.add(dish); result.put(dish.getType(), dishes); } else { //存在则追加 result.get(dish.getType()).add(dish); } } return result; }Java8以后的实现方式private static Map<Type, List<Dish>> afterJdk8(List<Dish> dishList) { return dishList.stream().collect(groupingBy(Dish::getType)); }{mtitle title="流的介绍"/}什么是流 流是从支持数据处理操作的源生成的元素序列,源可以是数组、文件、集合、函数。流不是集合元素,它不是数据结构并不保存数据,它的主要目的在于计算。如何生成流生成流的方式主要有五种:通过集合生成,应用中最常用的一种List integerList = Arrays.asList(1, 2, 3, 4, 5);Stream stream = integerList.stream();通过集合的stream方法生成流。通过数组生成int[] intArr = new int[]{1, 2, 3, 4, 5};IntStream stream = Arrays.stream(intArr);通过Arrays.stream方法生成流,并且该方法生成的流是数值流【即IntStream】而不是Stream。补充一点使用数值流可以避免计算过程中拆箱装箱,提高性能。Stream API提供了mapToInt、mapToDouble、mapToLong三种方式将对象流【即Stream】转换成对应的数值流,同时提供了boxed方法将数值流转换为对象流通过值生成Stream stream = Stream.of(1, 2, 3, 4, 5);通过Stream的of方法生成流,通过Stream的empty方法可以生成一个空流。通过文件生成Stream lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset()),通过Files.line方法得到一个流,并且得到的每个流是给定文件中的一行。通过函数生成 提供了iterate和generate两个静态方法从函数中生成流。iteratorStream stream = Stream.iterate(0, n -> n + 2).limit(5);iterate方法接受两个参数,第一个为初始化值,第二个为进行的函数操作,因为iterator生成的流为无限流,通过limit方法对流进行了截断,只生成5个偶数。generatorStream stream = Stream.generate(Math::random).limit(5);generate方法接受一个参数,方法参数类型为Supplier,由它为流提供值。generate生成的流也是无限流,因此通过limit对流进行了截断。流的操作类型流的操作类型主要分为两种中间操作 一个流可以后面跟随零个或多个中间操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的,仅仅调用到这类方法,并没有真正开始流的遍历,真正的遍历需等到终端操作时,常见的中间操作有下面即将介绍的filter、map等终端操作 一个流有且只能有一个终端操作,当这个操作执行后,流就被关闭了,无法再被操作,因此一个流只能被遍历一次,若想在遍历需要通过源数据在生成流。终端操作的执行,才会真正开始流的遍历。如下面即将介绍的count、collect等流使用流的使用将分为终端操作和中间操作进行介绍 中间操作 filter筛选List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5); Stream<Integer> stream = integerList.stream().filter(i -> i > 3);通过使用filter方法进行条件筛选,filter的方法参数为一个条件。 distinct去除重复元素List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5); Stream<Integer> stream = integerList.stream().distinct();通过distinct方法快速去除重复的元素。 limit返回指定流个数List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5); Stream<Integer> stream = integerList.stream().limit(3);通过limit方法指定返回流的个数,limit的参数值必须>=0,否则将会抛出异常。 skip跳过流中的元素List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5); Stream<Integer> stream = integerList.stream().skip(2);通过skip方法跳过流中的元素,上述例子跳过前两个元素,所以打印结果为2,3,4,5,skip的参数值必须>=0,否则将会抛出异常。 map流映射 所谓流映射就是将接受的元素映射成另外一个元素List<String> stringList = Arrays.asList("Java 8", "Lambdas", "In", "Action"); Stream<Integer> stream = stringList.stream().map(String::length);通过map方法可以完成映射,该例子完成中String -> Integer的映射,之前上面的例子通过map方法完成了Dish->String的映射。 flatMap流转换 将一个流中的每个值都转换为另一个流List<String> wordList = Arrays.asList("Hello", "World"); List<String> strList = wordList.stream() .map(w -> w.split(" ")) .flatMap(Arrays::stream) .distinct() .collect(Collectors.toList());map(w -> w.split(" "))的返回值为Stream<String[]>,我们想获取Stream,可以通过flatMap方法完成Stream<String[]> ->Stream的转换。元素匹配提供了三种匹配方式allMatch匹配所有List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); if (integerList.stream().allMatch(i -> i> 3)) { System.out.println("值都大于3"); }anyMatch匹配其中一个List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); if (integerList.stream().anyMatch(i -> i > 3)) { System.out.println("存在大于3的值"); } //等同于 for (Integer i : integerList) { if (i > 3) { System.out.println("存在大于3的值"); break; } }存在大于3的值则打印,java8中通过anyMatch方法实现这个功能noneMatch全部不匹配List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); if (integerList.stream().noneMatch(i -> i > 3)) { System.out.println("值都小于3"); }{mtitle title="终端操作"/}统计流中元素个数通过使用count方法统计出流中元素个数List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); Long result = integerList.stream().count();通过countingList<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); Long result = integerList.stream().collect(counting()); 最后一种统计元素个数的方法在与collect联合使用的时候特别有用。查找提供了两种查找方式findFirst查找第一个List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> result = integerList.stream().filter(i -> i > 3).findFirst();通过findFirst方法查找到第一个大于三的元素并打印findAny随机查找一个List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> result = integerList.stream().filter(i -> i > 3).findAny(); 通过findAny方法查找到其中一个大于三的元素并打印,因为内部进行优化的原因,当找到第一个满足大于三的元素时就结束,该方法结果和findFirst方法结果一样。提供findAny方法是为了更好的利用并行流,findFirst方法在并行上限制更多【本篇文章将不介绍并行流】reduce将流中的元素组合起来假设我们对一个集合中的值进行求和jdk8之前int sum = 0; for (int i : integerList) { sum += i; }jdk8之后通过reduce进行处理int sum = integerList.stream().reduce(0, (a, b) -> (a + b));一行就可以完成,还可以使用方法引用简写成:int sum = integerList.stream().reduce(0, Integer::sum); reduce接受两个参数,一个初始值这里是0,一个BinaryOperator accumulator 来将两个元素结合起来产生一个新值, 另外reduce方法还有一个没有初始化值的重载方法。获取流中最小最大值通过min/max获取最小最大值Optional<Integer> min = menu.stream().map(Dish::getCalories).min(Integer::compareTo); Optional<Integer> max = menu.stream().map(Dish::getCalories).max(Integer::compareTo);也可以写成:OptionalInt min = menu.stream().mapToInt(Dish::getCalories).min(); OptionalInt max = menu.stream().mapToInt(Dish::getCalories).max();min获取流中最小值,max获取流中最大值,方法参数为Comparator<? super T> comparator通过minBy/maxBy获取最小最大值Optional<Integer> min = menu.stream().map(Dish::getCalories).collect(minBy(Integer::compareTo)); Optional<Integer> max = menu.stream().map(Dish::getCalories).collect(maxBy(Integer::compareTo));minBy获取流中最小值,maxBy获取流中最大值,方法参数为Comparator<? super T> comparator通过reduce获取最小最大值Optional<Integer> min = menu.stream().map(Dish::getCalories).reduce(Integer::min); Optional<Integer> max = menu.stream().map(Dish::getCalories).reduce(Integer::max);求和通过summingIntintsum = menu.stream().collect(summingInt(Dish::getCalories)); 如果数据类型为double、long,则通过summingDouble、summingLong方法进行求和通过reduceintsum = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);通过sumintsum = menu.stream().mapToInt(Dish::getCalories).sum(); 在上面求和、求最大值、最小值的时候,对于相同操作有不同的方法可以选择执行。可以选择collect、reduce、min/max/sum方法,推荐使用min、max、sum方法。因为它最简洁易读,同时通过mapToInt将对象流转换为数值流,避免了装箱和拆箱操作。通过averagingInt求平均值double average = menu.stream().collect(averagingInt(Dish::getCalories));如果数据类型为double、long,则通过averagingDouble、averagingLong方法进行求平均。通过summarizingInt同时求总和、平均值、最大值、最小值IntSummaryStatistics intSummaryStatistics = menu.stream().collect(summarizingInt(Dish::getCalories)); double average = intSummaryStatistics.getAverage(); //获取平均值 int min = intSummaryStatistics.getMin(); //获取最小值 int max = intSummaryStatistics.getMax(); //获取最大值 long sum = intSummaryStatistics.getSum(); //获取总和如果数据类型为double、long,则通过summarizingDouble、summarizingLong方法。通过foreach进行元素遍历List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); integerList.stream().forEach(System.out::println);返回集合List<String> strings = menu.stream().map(Dish::getName).collect(toList()); Set<String> sets = menu.stream().map(Dish::getName).collect(toSet());只举例了一部分,还有很多其他方法 jdk8之前: List<String> stringList = new ArrayList<>(); Set<String> stringSet = new HashSet<>(); for (Dish dish : menu) { stringList.add(dish.getName()); stringSet.add(dish.getName()); }通过遍历和返回集合的使用发现流只是把原来的外部迭代放到了内部进行,这也是流的主要特点之一。内部迭代可以减少好多代码量。通过joining拼接流中的元素String result = menu.stream().map(Dish::getName).collect(Collectors.joining(", "));默认如果不通过map方法进行映射处理拼接的toString方法返回的字符串,joining的方法参数为元素的分界符,如果不指定生成的字符串将是一串的,可读性不强。进阶通过groupingBy进行分组Map<Type, List<Dish>> result = dishList.stream().collect(groupingBy(Dish::getType)); 在collect方法中传入groupingBy进行分组,其中groupingBy的方法参数为分类函数。还可以通过嵌套使用groupingBy进行多级分类。Map<Type, List<Dish>> result = menu.stream().collect(groupingBy(Dish::getType, groupingBy(dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET; else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; else return CaloricLevel.FAT; })));进阶通过partitioningBy进行分区分区是特殊的分组,它分类依据是true和false,所以返回的结果最多可以分为两组Map<Boolean, List<Dish>> result = menu.stream().collect(partitioningBy(Dish :: isVegetarian))等同于Map<Boolean, List<Dish>> result = menu.stream().collect(groupingBy(Dish :: isVegetarian))这个例子可能并不能看出分区和分类的区别,甚至觉得分区根本没有必要,换个明显一点的例子:List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); Map<Boolean, List<Integer>> result = integerList.stream().collect(partitioningBy(i -> i < 3)); 返回值的键仍然是布尔类型,但是它的分类是根据范围进行分类的,分区比较适合处理根据范围进行分类。
2022年05月01日
74 阅读
0 评论
0 点赞
2022-05-01
Java 语言实现简易版扫码登录
基本介绍 相信大家对二维码都不陌生,生活中到处充斥着扫码登录的场景,如登录网页版微信、支付宝等。最近学习了一下扫码登录的原理,感觉蛮有趣的,于是自己实现了一个简易版扫码登录的 Demo,以此记录一下学习过程。原理解析1.身份认证机制 在介绍扫码登录的原理之前,我们先聊一聊服务端的身份认证机制。以普通的 账号 + 密码 登录方式为例,服务端收到用户的登录请求后,首先验证账号、密码的合法性。如果验证通过,那么服务端会为用户分配一个 token,该 token 与用户的身份信息相关联,可作为用户的登录凭证。之后 PC 端再次发送请求时,需要在请求的 Header 或者 Query 参数中携带 token,服务端根据 token 便可识别出当前用户。token 的优点是更加方便、安全,它降低了账号密码被劫持的风险,而且用户不需要重复地输入账号和密码。PC 端通过账号和密码登录的过程如下: 扫码登录本质上也是一种身份认证方式,账号 + 密码 登录与扫码登录的区别在于,前者是利用 PC 端的账号和密码为 PC 端申请一个 token,后者是利用 手机端的 token + 设备信息 为 PC 端申请一个 token。这两种登录方式的目的相同,都是为了使 PC 端获得服务端的 "授权",在为 PC 端申请 token 之前,二者都需要向服务端证明自己的身份,也就是必须让服务端知道当前用户是谁,这样服务端才能为其生成 PC 端 token。由于扫码前手机端一定是处于已登录状态的,因此手机端本身已经保存了一个 token,该 token 可用于服务端的身份识别。那么为什么手机端在验证身份时还需要设备信息呢?实际上,手机端的身份认证和 PC 端略有不同:手机端在登录前也需要输入账号和密码,但登录请求中除了账号密码外还包含着设备信息,例如设备类型、设备 id 等。接收到登录请求后,服务端会验证账号和密码,验证通过后,将用户信息与设备信息关联起来,也就是将它们存储在一个数据结构 structure 中。服务端为手机端生成一个 token,并将 token 与用户信息、设备信息关联起来,即以 token 为 key,structure 为 value,将该键值对持久化保存到本地,之后将 token 返回给手机端。手机端发送请求,携带 token 和设备信息,服务端根据 token 查询出 structure,并验证 structure 中的设备信息和手机端的设备信息是否相同,以此判断用户的有效性。 我们在 PC 端登录成功后,可以短时间内正常浏览网页,但之后访问网站时就要重新登陆了,这是因为 token 是有过期时间的,较长的有效时间会增大 token 被劫持的风险。但是,手机端好像很少有这种问题,例如微信登录成功后可以一直使用,即使关闭微信或重启手机。这是因为设备信息具有唯一性,即使 token 被劫持了,由于设备信息不同,攻击者也无法向服务端证明自己的身份,这样大大提高了安全系数,因此 token 可以长久使用。手机端通过账号密码登录的过程如下:2.流程概述 了解了服务端的身份认证机制后,我们再聊一聊扫码登录的整个流程。以网页版微信为例,我们在 PC 端点击二维码登录后,浏览器页面会弹出二维码图片,此时打开手机微信扫描二维码,PC 端随即显示 "正在扫码",手机端点击确认登录后,PC 端就会显示 "登陆成功" 了。 上述过程中,服务端可以根据手机端的操作来响应 PC 端,那么服务端是如何将二者关联起来的呢?答案就是通过 "二维码",严格来说是通过二维码中的内容。使用二维码解码器扫描网页版微信的二维码,可以得到如下内容: 由上图我们得知,二维码中包含的其实是一个网址,手机扫描二维码后,会根据该网址向服务端发送请求。接着,我们打开 PC 端浏览器的开发者工具: 可见,在显示出二维码之后,PC 端一直都没有 "闲着",它通过轮询的方式不断向服务端发送请求,以获知手机端操作的结果。这里我们注意到,PC 端发送的 URL 中有一个参数 uuid,值为 "Adv-NP1FYw==",该 uuid 也存在于二维码包含的网址中。由此我们可以推断,服务端在生成二维码之前会先生成一个二维码 id,二维码 id 与二维码的状态、过期时间等信息绑定在一起,一同存储在服务端。手机端可以根据二维码 id 操作服务端二维码的状态,PC 端可以根据二维码 id 向服务端询问二维码的状态。 二维码最初为 "待扫描" 状态,手机端扫码后服务端将其状态改为 "待确认" 状态,此时 PC 端的轮询请求到达,服务端向其返回 "待确认" 的响应。手机端确认登录后,二维码变成 "已确认" 状态,服务端为 PC 端生成用于身份认证的 token,PC 端再次询问时,就可以得到这个 token。整个扫码登录的流程如下图所示:PC 端发送 "扫码登录" 请求,服务端生成二维码 id,并存储二维码的过期时间、状态等信息。PC 端获取二维码并显示。PC 端开始轮询检查二维码的状态,二维码最初为 "待扫描" 状态。手机端扫描二维码,获取二维码 id。手机端向服务端发送 "扫码" 请求,请求中携带二维码 id、手机端 token 以及设备信息。服务端验证手机端用户的合法性,验证通过后将二维码状态置为 "待确认",并将用户信息与二维码关联在一起,之后为手机端生成一个一次性 token,该 token 用作确认登录的凭证。PC 端轮询时检测到二维码状态为 "待确认"。手机端向服务端发送 "确认登录" 请求,请求中携带着二维码 id、一次性 token 以及设备信息。服务端验证一次性 token,验证通过后将二维码状态置为 "已确认",并为 PC 端生成 PC 端 token。PC 端轮询时检测到二维码状态为 "已确认",并获取到了 PC 端 token,之后 PC 端不再轮询。PC 端通过 PC 端 token 访问服务端。 上述过程中,我们注意到,手机端扫码后服务端会返回一个一次性 token,该 token 也是一种身份凭证,但它只能使用一次。一次性 token 的作用是确保 "扫码请求" 与 "确认登录" 请求由同一个手机端发出,也就是说,手机端用户不能 "帮其他用户确认登录"。 关于一次性 token 的知识本人也不是很了解,但可以推测,在服务端的缓存中,一次性 token 映射的 value 应该包含 "扫码" 请求传入的二维码信息、设备信息以及用户信息。代码实现1.生成二维码二维码的生成以及二维码状态的保存逻辑如下:@RequestMapping(path = "/getQrCodeImg", method = RequestMethod.GET) public String createQrCodeImg(Model model) { String uuid = loginService.createQrImg(); String qrCode = Base64.encodeBase64String(QrCodeUtil.generatePng("http://127.0.0.1:8080/login/uuid=" + uuid, 300, 300)); model.addAttribute("uuid", uuid); model.addAttribute("QrCode", qrCode); return "login"; } PC 端访问 "登录" 请求时,服务端调用 createQrImg 方法,生成一个 uuid 和一个 LoginTicket 对象,LoginTicket 对象中封装了用户的 userId 和二维码的状态。然后服务端将 uuid 作为 key,LoginTicket 对象作为 value 存入到 Redis 服务器中,并设置有效时间为 5 分钟(二维码的有效时间),createQrImg 方法的逻辑如下:public String createQrImg() { // uuid String uuid = CommonUtil.generateUUID(); LoginTicket loginTicket = new LoginTicket(); // 二维码最初为 WAITING 状态 loginTicket.setStatus(QrCodeStatusEnum.WAITING.getStatus()); // 存入 redis String ticketKey = CommonUtil.buildTicketKey(uuid); cacheStore.put(ticketKey, loginTicket, LoginConstant.WAIT_EXPIRED_SECONDS, TimeUnit.SECONDS); return uuid; } 我们在前一节中提到,手机端的操作主要影响二维码的状态,PC 端轮询时也是查看二维码的状态,那么为什么还要在 LoginTicket 对象中封装 userId 呢?这样做是为了将二维码与用户进行关联,想象一下我们登录网页版微信的场景,手机端扫码后,PC 端就会显示用户的头像,虽然手机端并未确认登录,但 PC 端轮询时已经获取到了当前扫码的用户(仅头像信息)。因此手机端扫码后,需要将二维码与用户绑定在一起,使用 LoginTicket 对象只是一种实现方式。二维码生成后,我们将其状态置为 "待扫描" 状态,userId 不做处理,默认为 null。2.扫描二维码 手机端发送 "扫码" 请求时,Query 参数中携带着 uuid,服务端接收到请求后,调用 scanQrCodeImg 方法,根据 uuid 查询出二维码并将其状态置为 "待确认" 状态,操作完成后服务端向手机端返回 "扫码成功" 或 "二维码已失效" 的信息:@RequestMapping(path = "/scan", method = RequestMethod.POST) @ResponseBody public Response scanQrCodeImg(@RequestParam String uuid) { JSONObject data = loginService.scanQrCodeImg(uuid); if (data.getBoolean("valid")) { return Response.createResponse("扫码成功", data); } return Response.createErrorResponse("二维码已失效"); }scanQrCodeImg 方法的主要逻辑如下:public JSONObject scanQrCodeImg(String uuid) { // 避免多个移动端同时扫描同一个二维码 lock.lock(); JSONObject data = new JSONObject(); try { String ticketKey = CommonUtil.buildTicketKey(uuid); LoginTicket loginTicket = (LoginTicket) cacheStore.get(ticketKey); // redis 中 key 过期后也可能不会立即删除 Long expired = cacheStore.getExpireForSeconds(ticketKey); boolean valid = loginTicket != null && QrCodeStatusEnum.parse(loginTicket.getStatus()) == QrCodeStatusEnum.WAITING && expired != null && expired >= 0; if (valid) { User user = hostHolder.getUser(); if (user == null) { throw new RuntimeException("用户未登录"); } // 修改扫码状态 loginTicket.setStatus(QrCodeStatusEnum.SCANNED.getStatus()); Condition condition = CONDITION_CONTAINER.get(uuid); if (condition != null) { condition.signal(); CONDITION_CONTAINER.remove(uuid); } // 将二维码与用户进行关联 loginTicket.setUserId(user.getUserId()); cacheStore.put(ticketKey, loginTicket, expired, TimeUnit.SECONDS); // 生成一次性 token, 用于之后的确认请求 String onceToken = CommonUtil.generateUUID(); cacheStore.put(CommonUtil.buildOnceTokenKey(onceToken), uuid, LoginConstant.ONCE_TOKEN_EXPIRE_TIME, TimeUnit.SECONDS); data.put("once_token", onceToken); } data.put("valid", valid); return data; } finally { lock.unlock(); } }首先根据 uuid 查询 Redis 中存储的 LoginTicket 对象,然后检查二维码的状态是否为 "待扫描" 状态,如果是,那么将二维码的状态改为 "待确认" 状态。如果不是,那么该二维码已被扫描过,服务端提示用户 "二维码已失效"。我们规定,只允许第一个手机端能够扫描成功,加锁的目的是为了保证 查询 + 修改 操作的原子性,避免两个手机端同时扫码,且同时检测到二维码的状态为 "待扫描"。上一步操作成功后,服务端将 LoginTicket 对象中的 userId 置为当前用户(扫码用户)的 userId,也就是将二维码与用户信息绑定在一起。由于扫码请求是由手机端发送的,因此该请求一定来自于一个有效的用户,我们在项目中配置一个拦截器(也可以是过滤器),当拦截到 "扫码" 请求后,根据请求中的 token(手机端发送请求时一定会携带 token)查询出用户信息,并将其存储到 ThreadLocal 容器(hostHolder)中,之后绑定信息时就可以从 ThreadLocal 容器将用户信息提取出来。注意,这里的 token 指的手机端 token,实际中应该还有设备信息,但为了简化操作,我们忽略掉设备信息。用户信息与二维码信息关联在一起后,服务端为手机端生成一个一次性 token,并存储到 Redis 服务器,其中 key 为一次性 token 的值,value 为 uuid。一次性 token 会返回给手机端,作为 "确认登录" 请求的凭证。 上述代码中,当二维码的状态被修改后,我们唤醒了在 condition 中阻塞的线程,这一步的目的是为了实现长轮询操作,下文中会介绍长轮询的设计思路。3.确认登录 手机端发送 "确认登录" 请求时,Query 参数中携带着 uuid,且 Header 中携带着一次性 token,服务端接收到请求后,首先验证一次性 token 的有效性,即检查一次性 token 对应的 uuid 与 Query 参数中的 uuid 是否相同,以确保扫码操作和确认操作来自于同一个手机端,该验证过程可在拦截器中配置。验证通过后,服务端调用 confirmLogin 方法,将二维码的状态置为 "已确认":@RequestMapping(path = "/confirm", method = RequestMethod.POST) @ResponseBody public Response confirmLogin(@RequestParam String uuid) { boolean logged = loginService.confirmLogin(uuid); String msg = logged ? "登录成功!" : "二维码已失效!"; return Response.createResponse(msg, logged); }confirmLogin 方法的主要逻辑如下:public boolean confirmLogin(String uuid) { String ticketKey = CommonUtil.buildTicketKey(uuid); LoginTicket loginTicket = (LoginTicket) cacheStore.get(ticketKey); boolean logged = true; Long expired = cacheStore.getExpireForSeconds(ticketKey); if (loginTicket == null || expired == null || expired == 0) { logged = false; } else { lock.lock(); try { loginTicket.setStatus(QrCodeStatusEnum.CONFIRMED.getStatus()); Condition condition = CONDITION_CONTAINER.get(uuid); if (condition != null) { condition.signal(); CONDITION_CONTAINER.remove(uuid); } cacheStore.put(ticketKey, loginTicket, expired, TimeUnit.SECONDS); } finally { lock.unlock(); } } return logged; } 该方法会根据 uuid 查询二维码是否已经过期,如果未过期,那么就修改二维码的状态。4.PC 端轮询 轮询操作指的是前端重复多次向后端发送相同的请求,以获知数据的变化。轮询分为长轮询和短轮询:长轮询:服务端收到请求后,如果有数据,那么就立即返回,否则线程进入等待状态,直到有数据到达或超时,浏览器收到响应后立即重新发送相同的请求。短轮询:服务端收到请求后无论是否有数据都立即返回,浏览器收到响应后间隔一段时间后重新发送相同的请求。 由于长轮询相比短轮询能够得到实时的响应,且更加节约资源,因此项目中我们考虑使用 ReentrantLock 来实现长轮询。轮询的目的是为了查看二维码状态的变化:@RequestMapping(path = "/getQrCodeStatus", method = RequestMethod.GET) @ResponseBody public Response getQrCodeStatus(@RequestParam String uuid, @RequestParam int currentStatus) throws InterruptedException { JSONObject data = loginService.getQrCodeStatus(uuid, currentStatus); return Response.createResponse(null, data); }getQrCodeStatus 方法的主要逻辑如下:public JSONObject getQrCodeStatus(String uuid, int currentStatus) throws InterruptedException { lock.lock(); try { JSONObject data = new JSONObject(); String ticketKey = CommonUtil.buildTicketKey(uuid); LoginTicket loginTicket = (LoginTicket) cacheStore.get(ticketKey); QrCodeStatusEnum statusEnum = loginTicket == null || QrCodeStatusEnum.parse(loginTicket.getStatus()) == QrCodeStatusEnum.INVALID ? QrCodeStatusEnum.INVALID : QrCodeStatusEnum.parse(loginTicket.getStatus()); if (currentStatus == statusEnum.getStatus()) { Condition condition = CONDITION_CONTAINER.get(uuid); if (condition == null) { condition = lock.newCondition(); CONDITION_CONTAINER.put(uuid, condition); } condition.await(LoginConstant.POLL_WAIT_TIME, TimeUnit.SECONDS); } // 用户扫码后向 PC 端返回头像信息 if (statusEnum == QrCodeStatusEnum.SCANNED) { User user = userService.getCurrentUser(loginTicket.getUserId()); data.put("avatar", user.getAvatar()); } // 用户确认后为 PC 端生成 access_token if (statusEnum == QrCodeStatusEnum.CONFIRMED) { String accessToken = CommonUtil.generateUUID(); cacheStore.put(CommonUtil.buildAccessTokenKey(accessToken), loginTicket.getUserId(), LoginConstant.ACCESS_TOKEN_EXPIRE_TIME, TimeUnit.SECONDS); data.put("access_token", accessToken); } data.put("status", statusEnum.getStatus()); data.put("message", statusEnum.getMessage()); return data; } finally { lock.unlock(); } } 该方法接收两个参数,即 uuid 和 currentStatus,其中 uuid 用于查询二维码,currentStatus 用于确认二维码状态是否发生了变化,如果是,那么需要立即向 PC 端反馈。我们规定 PC 端在轮询时,请求的参数中需要携带二维码当前的状态。首先根据 uuid 查询出二维码的最新状态,并比较其是否与 currentStatus 相同。如果相同,那么当前线程进入阻塞状态,直到被唤醒或者超时。如果二维码状态为 "待确认",那么服务端向 PC 端返回扫码用户的头像信息(处于 "待确认" 状态时,二维码已与用户信息绑定在一起,因此可以查询出用户的头像)。如果二维码状态为 "已确认",那么服务端为 PC 端生成一个 token,在之后的请求中,PC 端可通过该 token 表明自己的身份。上述代码中的加锁操作是为了能够令当前处理请求的线程进入阻塞状态,当二维码的状态发生变化时,我们再将其唤醒,因此上文中的扫码操作和确认登录操作完成后,还会有一个唤醒线程的过程。 实际上,加锁操作设计得不太合理,因为我们只设置了一把锁。因此对不同二维码的查询或修改操作都会抢占同一把锁。按理来说,不同二维码的操作之间应该是相互独立的,即使加锁,也应该是为每个二维码均配一把锁,但这样做代码会更加复杂,或许有其它更好的实现长轮询的方式?或者干脆直接短轮询。当然,也可以使用 WebSocket 实现长连接。5. 拦截器配置 项目中配置了两个拦截器,一个用于确认用户的身份,即验证 token 是否有效:@Component public class LoginInterceptor implements HandlerInterceptor { @Autowired private HostHolder hostHolder; @Autowired private CacheStore cacheStore; @Autowired private UserService userService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String accessToken = request.getHeader("access_token"); // access_token 存在 if (StringUtils.isNotEmpty(accessToken)) { String userId = (String) cacheStore.get(CommonUtil.buildAccessTokenKey(accessToken)); User user = userService.getCurrentUser(userId); hostHolder.setUser(user); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { hostHolder.clear(); } } 如果 token 有效,那么服务端根据 token 获取用户的信息,并将用户信息存储到 ThreadLocal 容器。手机端和 PC 端的请求都由该拦截器处理,如 PC 端的 "查询用户信息" 请求,手机端的 "扫码" 请求。由于我们忽略了手机端验证时所需要的的设备信息,因此 PC 端和手机端 token 可以使用同一套验证逻辑。 另一个拦截器用于拦截 "确认登录" 请求,即验证一次性 token 是否有效:@Component public class ConfirmInterceptor implements HandlerInterceptor { @Autowired private CacheStore cacheStore; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String onceToken = request.getHeader("once_token"); if (StringUtils.isEmpty(onceToken)) { return false; } if (StringUtils.isNoneEmpty(onceToken)) { String onceTokenKey = CommonUtil.buildOnceTokenKey(onceToken); String uuidFromCache = (String) cacheStore.get(onceTokenKey); String uuidFromRequest = request.getParameter("uuid"); if (!StringUtils.equals(uuidFromCache, uuidFromRequest)) { throw new RuntimeException("非法的一次性 token"); } // 一次性 token 检查完成后将其删除 cacheStore.delete(onceTokenKey); } return true; } } 该拦截器主要拦截 "确认登录" 请求,需要注意的是,一次性 token 验证通过后要立即将其删除。 编码过程中,我们简化了许多操作,例如:1. 忽略掉了手机端的设备信息;2. 手机端确认登录后并没有直接为用户生成 PC 端 token,而是在轮询时生成。
2022年05月01日
35 阅读
0 评论
0 点赞
2022-05-01
解决Mybatis-Plus批量插入数据太慢,堪称神速
前言 用过Mybatis-Plus的小伙伴一定知道他有很多API提供给我们使用,真爽,再不用写那么多繁琐的SQL语句,saveBatch是Plus的批量插入函数,大家平时工作肯定都用过,下面我们就来一个案例进入今天的主题。rewriteBatchedStatements参数 MySQL的JDBC连接的url中要加rewriteBatchedStatements参数,并保证5.1.13以上版本的驱动,才能实现高性能的批量插入。MySQL JDBC驱动在默认情况下会无视executeBatch()语句,把我们期望批量执行的一组sql语句拆散,一条一条地发给MySQL数据库,批量插入实际上是单条插入,直接造成较低的性能。只有把rewriteBatchedStatements参数置为true, 驱动才会帮你批量执行SQL,另外这个选项对INSERT/UPDATE/DELETE都有效。
2022年05月01日
195 阅读
0 评论
0 点赞
2022-04-29
推荐好用 SpringBoot 内置工具类
断言断言是一个逻辑判断,用于检查不应该发生的情况Assert 关键字在 JDK1.4 中引入,可通过 JVM 参数-enableassertions开启SpringBoot 中提供了 Assert 断言工具类,通常用于数据合法性检查// 要求参数 object 必须为非空(Not Null),否则抛出异常,不予放行 // 参数 message 参数用于定制异常信息。 void notNull(Object object, String message) // 要求参数必须空(Null),否则抛出异常,不予『放行』。 // 和 notNull() 方法断言规则相反 void isNull(Object object, String message) // 要求参数必须为真(True),否则抛出异常,不予『放行』。 void isTrue(boolean expression, String message) // 要求参数(List/Set)必须非空(Not Empty),否则抛出异常,不予放行 void notEmpty(Collection collection, String message) // 要求参数(String)必须有长度(即,Not Empty),否则抛出异常,不予放行 void hasLength(String text, String message) // 要求参数(String)必须有内容(即,Not Blank),否则抛出异常,不予放行 void hasText(String text, String message) // 要求参数是指定类型的实例,否则抛出异常,不予放行 void isInstanceOf(Class type, Object obj, String message) // 要求参数 `subType` 必须是参数 superType 的子类或实现类,否则抛出异常,不予放行 void isAssignable(Class superType, Class subType, String message)对象、数组、集合ObjectUtils获取对象的基本信息// 获取对象的类名。参数为 null 时,返回字符串:"null" String nullSafeClassName(Object obj) // 参数为 null 时,返回 0 int nullSafeHashCode(Object object) // 参数为 null 时,返回字符串:"null" String nullSafeToString(boolean[] array) // 获取对象 HashCode(十六进制形式字符串)。参数为 null 时,返回 0 String getIdentityHexString(Object obj) // 获取对象的类名和 HashCode。 参数为 null 时,返回字符串:"" String identityToString(Object obj) // 相当于 toString()方法,但参数为 null 时,返回字符串:"" String getDisplayString(Object obj)判断工具// 判断数组是否为空 boolean isEmpty(Object[] array) // 判断参数对象是否是数组 boolean isArray(Object obj) // 判断数组中是否包含指定元素 boolean containsElement(Object[] array, Object element) // 相等,或同为 null时,返回 true boolean nullSafeEquals(Object o1, Object o2) /* 判断参数对象是否为空,判断标准为: Optional: Optional.empty() Array: length == 0 CharSequence: length == 0 Collection: Collection.isEmpty() Map: Map.isEmpty() */ boolean isEmpty(Object obj)其他工具方法// 向参数数组的末尾追加新元素,并返回一个新数组 <A, O extends A> A[] addObjectToArray(A[] array, O obj) // 原生基础类型数组 --> 包装类数组 Object[] toObjectArray(Object source)StringUtils字符串判断工具// 判断字符串是否为 null,或 ""。注意,包含空白符的字符串为非空 boolean isEmpty(Object str) // 判断字符串是否是以指定内容结束。忽略大小写 boolean endsWithIgnoreCase(String str, String suffix) // 判断字符串是否已指定内容开头。忽略大小写 boolean startsWithIgnoreCase(String str, String prefix) // 是否包含空白符 boolean containsWhitespace(String str) // 判断字符串非空且长度不为 0,即,Not Empty boolean hasLength(CharSequence str) // 判断字符串是否包含实际内容,即非仅包含空白符,也就是 Not Blank boolean hasText(CharSequence str) // 判断字符串指定索引处是否包含一个子串。 boolean substringMatch(CharSequence str, int index, CharSequence substring) // 计算一个字符串中指定子串的出现次数 int countOccurrencesOf(String str, String sub)字符串操作工具// 查找并替换指定子串 String replace(String inString, String oldPattern, String newPattern) // 去除尾部的特定字符 String trimTrailingCharacter(String str, char trailingCharacter) // 去除头部的特定字符 String trimLeadingCharacter(String str, char leadingCharacter) // 去除头部的空白符 String trimLeadingWhitespace(String str) // 去除头部的空白符 String trimTrailingWhitespace(String str) // 去除头部和尾部的空白符 String trimWhitespace(String str) // 删除开头、结尾和中间的空白符 String trimAllWhitespace(String str) // 删除指定子串 String delete(String inString, String pattern) // 删除指定字符(可以是多个) String deleteAny(String inString, String charsToDelete) // 对数组的每一项执行 trim() 方法 String[] trimArrayElements(String[] array) // 将 URL 字符串进行解码 String uriDecode(String source, Charset charset)路径相关工具方法// 解析路径字符串,优化其中的 “..” String cleanPath(String path) // 解析路径字符串,解析出文件名部分 String getFilename(String path) // 解析路径字符串,解析出文件后缀名 String getFilenameExtension(String path) // 比较两个两个字符串,判断是否是同一个路径。会自动处理路径中的 “..” boolean pathEquals(String path1, String path2) // 删除文件路径名中的后缀部分 String stripFilenameExtension(String path) // 以 “. 作为分隔符,获取其最后一部分 String unqualify(String qualifiedName) // 以指定字符作为分隔符,获取其最后一部分 String unqualify(String qualifiedName, char separator)CollectionUtils集合判断工具// 判断 List/Set 是否为空 boolean isEmpty(Collection<?> collection) // 判断 Map 是否为空 boolean isEmpty(Map<?,?> map) // 判断 List/Set 中是否包含某个对象 boolean containsInstance(Collection<?> collection, Object element) // 以迭代器的方式,判断 List/Set 中是否包含某个对象 boolean contains(Iterator<?> iterator, Object element) // 判断 List/Set 是否包含某些对象中的任意一个 boolean containsAny(Collection<?> source, Collection<?> candidates) // 判断 List/Set 中的每个元素是否唯一。即 List/Set 中不存在重复元素 boolean hasUniqueObject(Collection<?> collection)集合操作工具// 将 Array 中的元素都添加到 List/Set 中 <E> void mergeArrayIntoCollection(Object array, Collection<E> collection) // 将 Properties 中的键值对都添加到 Map 中 <K,V> void mergePropertiesIntoMap(Properties props, Map<K,V> map) // 返回 List 中最后一个元素 <T> T lastElement(List<T> list) // 返回 Set 中最后一个元素 <T> T lastElement(Set<T> set) // 返回参数 candidates 中第一个存在于参数 source 中的元素 <E> E findFirstMatch(Collection<?> source, Collection<E> candidates) // 返回 List/Set 中指定类型的元素。 <T> T findValueOfType(Collection<?> collection, Class<T> type) // 返回 List/Set 中指定类型的元素。如果第一种类型未找到,则查找第二种类型,以此类推 Object findValueOfType(Collection<?> collection, Class<?>[] types) // 返回 List/Set 中元素的类型 Class<?> findCommonElementType(Collection<?> collection)文件、资源、IO 流FileCopyUtils输入// 从文件中读入到字节数组中 byte[] copyToByteArray(File in) // 从输入流中读入到字节数组中 byte[] copyToByteArray(InputStream in) // 从输入流中读入到字符串中 String copyToString(Reader in)输出// 从字节数组到文件 void copy(byte[] in, File out) // 从文件到文件 int copy(File in, File out) // 从字节数组到输出流 void copy(byte[] in, OutputStream out) // 从输入流到输出流 int copy(InputStream in, OutputStream out) // 从输入流到输出流 int copy(Reader in, Writer out) // 从字符串到输出流 void copy(String in, Writer out)ResourceUtils从资源路径获取文件// 判断字符串是否是一个合法的 URL 字符串。 static boolean isUrl(String resourceLocation) // 获取 URL static URL getURL(String resourceLocation) // 获取文件(在 JAR 包内无法正常使用,需要是一个独立的文件) static File getFile(String resourceLocation)Resource// 文件系统资源 D:\... FileSystemResource // URL 资源,如 file://... http://... UrlResource // 类路径下的资源,classpth:... ClassPathResource // Web 容器上下文中的资源(jar 包、war 包) ServletContextResource // 判断资源是否存在 boolean exists() // 从资源中获得 File 对象 File getFile() // 从资源中获得 URI 对象 URI getURI() // 从资源中获得 URI 对象 URL getURL() // 获得资源的 InputStream InputStream getInputStream() // 获得资源的描述信息 String getDescription()StreamUtils输入void copy(byte[] in, OutputStream out) int copy(InputStream in, OutputStream out) void copy(String in, Charset charset, OutputStream out) long copyRange(InputStream in, OutputStream out, long start, long end)输出byte[] copyToByteArray(InputStream in) String copyToString(InputStream in, Charset charset) // 舍弃输入流中的内容 int drain(InputStream in)反射、AOPReflectionUtils获取方法// 在类中查找指定方法 Method findMethod(Class<?> clazz, String name) // 同上,额外提供方法参数类型作查找条件 Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes) // 获得类中所有方法,包括继承而来的 Method[] getAllDeclaredMethods(Class<?> leafClass) // 在类中查找指定构造方法 Constructor<T> accessibleConstructor(Class<T> clazz, Class<?>... parameterTypes) // 是否是 equals() 方法 boolean isEqualsMethod(Method method) // 是否是 hashCode() 方法 boolean isHashCodeMethod(Method method) // 是否是 toString() 方法 boolean isToStringMethod(Method method) // 是否是从 Object 类继承而来的方法 boolean isObjectMethod(Method method) // 检查一个方法是否声明抛出指定异常 boolean declaresException(Method method, Class<?> exceptionType)执行方法// 执行方法 Object invokeMethod(Method method, Object target) // 同上,提供方法参数 Object invokeMethod(Method method, Object target, Object... args) // 取消 Java 权限检查。以便后续执行该私有方法 void makeAccessible(Method method) // 取消 Java 权限检查。以便后续执行私有构造方法 void makeAccessible(Constructor<?> ctor)获取字段// 在类中查找指定属性 Field findField(Class<?> clazz, String name) // 同上,多提供了属性的类型 Field findField(Class<?> clazz, String name, Class<?> type) // 是否为一个 "public static final" 属性 boolean isPublicStaticFinal(Field field)设置字段// 获取 target 对象的 field 属性值 Object getField(Field field, Object target) // 设置 target 对象的 field 属性值,值为 value void setField(Field field, Object target, Object value) // 同类对象属性对等赋值 void shallowCopyFieldState(Object src, Object dest) // 取消 Java 的权限控制检查。以便后续读写该私有属性 void makeAccessible(Field field) // 对类的每个属性执行 callback void doWithFields(Class<?> clazz, ReflectionUtils.FieldCallback fc) // 同上,多了个属性过滤功能。 void doWithFields(Class<?> clazz, ReflectionUtils.FieldCallback fc, ReflectionUtils.FieldFilter ff) // 同上,但不包括继承而来的属性 void doWithLocalFields(Class<?> clazz, ReflectionUtils.FieldCallback fc)AopUtils判断代理类型// 判断是不是 Spring 代理对象 boolean isAopProxy() // 判断是不是 jdk 动态代理对象 isJdkDynamicProxy() // 判断是不是 CGLIB 代理对象 boolean isCglibProxy()获取被代理对象的 class// 获取被代理的目标 class Class<?> getTargetClass()AopContext获取当前对象的代理对象Object currentProxy()
2022年04月29日
33 阅读
0 评论
0 点赞
2022-04-25
Mac 下SSH到centos服务器
自从换了MacBook Pro ,大大提高了生产力,使用过程中发现自带的终端特别好用,可以取代用了很久的xshell,捣鼓了一下分享一下经验。# 添加公钥到服务器 cat id_rsa.pub >> ~/.ssh/authorized_keys # 创建快捷连接 echo "alias ssh-to-服务器别名='ssh root@服务器ip'" >> ~/.zshrc # 使.zshrc生效 source ~/.zshrc
2022年04月25日
59 阅读
0 评论
0 点赞
2022-04-22
阿里巴巴开发手册分享(二)MySQL规约
{mtitle title="建表规约"/}【强制】表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(1 表示是,0 表示否)。注意:POJO 类中的任何布尔类型的变量,都不要加 is 前缀,所以,需要在设置从 is_xxx 到 Xxx 的映射关系(比如:isUsed->Used)。数据库表示是与否的值,使用 tinyint 类型,坚持 is_xxx 的命名方式是为了明确其取值含义与取值范围。说明:任何字段如果为非负数,必须是 unsigned。正例:表达逻辑删除的字段名 is_deleted,1 表示删除,0 表示未删除【强制】表名、字段名必须使用小写字母或数字,禁止出现数字开头禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。说明:MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。正例:aliyun_admin,rdc_config,level3_name反例:AliyunAdmin,rdcConfig,level_3_name【强制】表名不使用复数名词。说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。【强制】禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字。【强制】主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。说明:pk_即 primary key;uk_即 unique key;idx_即 index 的简称。【强制】小数类型为 decimal,禁止使用 float 和 double。说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。【强制】如果存储的字符串长度几乎相等,使用 char 定长字符串类型。【强制】varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引率。【强制】表必备三字段:id,create_time,update_time。说明:其中 id 必为主键,类型为 bigint unsigned、单表时自增、步长为 1。create_time,update_time 的类型均为datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新。【强制】在数据库中不能使用物理删除操作,要使用逻辑删除。说明:逻辑删除在数据删除后可以追溯到行为操作。不过会使得一些情况下的唯一主键变得不唯一,需要根据情况来酌情解决。【推荐】表的命名最好是遵循“业务名称_表的作用”。 正例:alipay_task / force_project / trade_config / tes_question【推荐】库名与应用名称尽量一致【推荐】如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循: 1)不是频繁修改的字段。 2)不是唯一索引的字段。3)不是 varchar 超长字段,更不能是 text 字段。正例:各业务线经常冗余存储商品名称,避免查询时需要调用 IC 服务获取。【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。正例:无符号值可以避免误存负数,且扩大了表示范围:{mtitle title="索引规约"/}【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。【强制】超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引。说明:即使双表 join 也要注意表索引、SQL 性能。【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名,索引长度)) / count(*) 的区分度来确定。【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。【推荐】如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 filesort 的情况,影响查询性能。正例:where a = ? and b = ? order by c;索引:a_b_c反例:索引如果存在范围查询,那么索引有序性无法利用,如:WHERE a > 10 ORDER BY b;索引 a_b 无法排序。【推荐】利用覆盖索引来进行查询操作,避免回表。说明:如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。正例:能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查询的一种效果,用 explain的结果,extra 列会出现:using index【推荐】利用延迟关联或者子查询优化超多分页场景。说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。正例:先快速定位需要获取的 id 段,然后再关联:SELECT t1.* FROM 表 1 as t1 , (select id from 表 1 where 条件 LIMIT 100000 , 20) as t2 where t1.id = t2.id【推荐】SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 const 最好。说明: 1)consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。2)ref 指的是使用普通的索引(normal index)。3)range 对索引进行范围检索。反例:explain 表的结果,type = index,索引物理文件全扫描,速度非常慢,这个 index 级别比较 range 还低,与全表扫描是小巫见大巫。【推荐】建组合索引的时候,区分度最高的在最左边。正例:如果 where a = ? and b = ?,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where c > ? and d = ? 那么即使c 的区分度更高,也必须把 d 放在索引的最前列,即建立组合索引 idx_d_c。【推荐】防止因字段类型不同造成的隐式转换,导致索引失效。【参考】创建索引时避免有如下极端误解:1)索引宁滥勿缺。认为一个查询就需要建一个索引。2)吝啬索引的创建。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度。3)抵制唯一索引。认为唯一索引一律需要在应用层通过“先查后插”方式解决。{mtitle title="SQL 语句"/}【强制】不要使用 count(列名) 或 count(常量) 来替代 count(),count() 是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。说明:count(*) 会统计值为 NULL 的行,而 count(列名) 不会统计此列为 NULL 值的行。【强制】count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1 , col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0【强制】当某一列的值全是 NULL 时,count(col) 的返回结果为 0;但 sum(col) 的返回结果为 NULL,因此使用 sum() 时需注意 NPE 问题。正例:可以使用如下方式来避免 sum 的 NPE 问题:SELECT IFNULL(SUM(column) , 0) FROM table;【强制】使用 ISNULL() 来判断是否为 NULL 值。说明:NULL 与任何值的直接比较都为 NULL。1)NULL<>NULL 的返回结果是 NULL,而不是 false。 2)NULL=NULL 的返回结果是 NULL,而不是 true。 3)NULL<>1 的返回结果是 NULL,而不是 true。反例:在 SQL 语句中,如果在 null 前换行,影响可读性。select * from table where column1 is null and column3 is not null;而 ISNULL(column) 是一个整体,简洁易懂。从性能数据上分析,ISNULL(column) 执行效率更快一些。【强制】代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。【强制】不得使用外键与级联,一切外键概念必须在应用层解决。说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。【强制】数据订正(特别是删除或修改记录操作)时,要先 select,避免出现误删除的情况,确认无误才能执行更新语句。【强制】对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或表名)进行限定。说明:对多表进行查询记录、更新记录、删除记录时,如果对操作列没有限定表的别名(或表名),并且操作列在多个表中存在时,就会抛异常。正例:select t1.name from first_table as t1 , second_table as t2 where t1.id = t2.id;反例:在某业务中,由于多表关联查询语句没有加表的别名(或表名)的限制,正常运行两年后,最近在某个表中增加一个同名字段,在预发布环境做数据库变更后,线上查询语句出现出 1052 异常:Column 'name' infield list is ambiguous。【推荐】SQL 语句中表的别名前加 as,并且以 t1、t2、t3、...的顺序依次命名。说明: 1)别名可以是表的简称,或者是依照表在 SQL 语句中出现的顺序,以 t1、t2、t3 的方式命名。2)别名前加 as 使别名更容易识别。正例:select t1.name from first_table as t1 , second_table as t2 where t1.id = t2.id;【推荐】in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在1000 个之内。【参考】因国际化需要,所有的字符存储与表示,均采用 utf8 字符集,那么字符计数方法需要注意。说明:SELECT LENGTH("轻松工作");--返回为 12SELECT CHARACTER_LENGTH("轻松工作");--返回为 4如果需要存储表情,那么选择 utf8mb4 来进行存储,注意它与 utf8 编码的区别。【参考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句。说明:TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。{mtitle title="ORM 映射"/}【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。说明: 1)增加查询分析器解析成本。 2)增减字段容易与 resultMap 配置不一致。 3)无用字段增加网络消耗,尤其是 text 类型的字段。【强制】POJO 类的布尔属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性之间的映射。说明:参见定义 POJO 类以及数据库字段定义规定,在 sql.xml 增加映射,是必须的。【强制】不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必然有一个与之对应。说明:配置映射关系,使字段与 DO 类解耦,方便维护。【强制】sql.xml 配置参数使用:#{},#param# 不要使用 ${} 此种方式容易出现 SQL 注入。【强制】iBATIS 自带的 queryForList(String statementName,int start,int size) 不推荐使用。说明:其实现方式是在数据库取到 statementName 对应的 SQL 语句的所有记录,再通过 subList 取 start,size的子集合,线上因为这个原因曾经出现过 OOM。正例:Map<String, Object> map = new HashMap<>(16); map.put("start", start); map.put("size", size);【强制】不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出。反例:某同学为避免写一个xxx,直接使用 Hashtable 来接收数据库返回结果,结果出现日常是把 bigint 转成 Long 值,而线上由于数据库版本不一样,解析成 BigInteger,导致线上问题。【强制】更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。【推荐】不要写一个大而全的数据更新接口。传入为 POJO 类,不管是不是自己的目标更新字段,都进行update table set c1 = value1 , c2 = value2 , c3 = value3;这是不对的。执行 SQL 时,不要更新无改动的字段,一是易出错;二是效率低;三是增加 binlog 存储。【参考】@Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。【参考】中的 compareValue 是与属性值对比的常量,一般是数字,表示相等时带上此条件;表示不为空且不为 null 时执行;表示不为 null 值时执行。
2022年04月22日
60 阅读
0 评论
0 点赞
2022-04-22
阿里巴巴开发手册分享(一)编程规约
虽然在平时的开发过程中也很注意一些代码风格,但是阅读了阿里巴巴开发手册,发现自己还有很多不规范的地方,在此,也将平时经常弄错的地方总结一下。{mtitle title="命名风格"/}【强制】类名使用 UpperCamelCase 风格,以下情形例外:DO / PO / DTO / BO / VO / UID 等正例:ForceCode / UserDO / HtmlDTO / XmlService / TcpUdpDeal / TaPromotion【强制】方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格正例:localValue / getHttpMessage() / inputUserId【强制】常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME【强制】抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾,测试类命名以它要测试的类的名称开始,以 Test 结尾【强制】类型与中括号紧挨相连来定义数组正例:定义整形数组 int[] arrayDemo【强制】POJO 类中的任何布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。说明:本文 MySQL 规约中的建表约定第 1 条,表达是与否的变量采用 is_xxx 的命名方式,所以需要在设置从 is_xxx 到 xxx 的映射关系。【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形 式,但是类名如果有复数含义,类名可以使用复数形式。正例:应用工具类包名为 com.alibaba.ei.kunlun.aap.util;类名为 MessageUtils(此规则参考 spring 的框架结构)。【强制】避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。public class ConfusingName { protected int stock; protected String alibaba; // 非 setter/getter 的参数名称,不允许与本类成员变量同名 public void access(String alibaba) { if (condition) { final int money = 666; // ... } for (int i = 0; i < 10; i++) { // 在同一方法体中,不允许与其它代码块中的 money 命名相同 final int money = 15978; // ... } } } class Son extends ConfusingName { // 不允许与父类的成员变量名称相同 private int stock; }【推荐】为了达到代码自解释的目标,任何自定义编程元素在命名时,使用完整的单词组合来表达。正例:在 JDK 中,对某个对象引用的 volatile 字段进行原子更新的类名为 AtomicReferenceFieldUpdater。反例:常见的方法内变量为 int a; 的定义方式。【推荐】如果模块、接口、类、方法使用了设计模式,在命名时要体现出具体模式。说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计思想。正例: public class OrderFactory;public class LoginProxy;public class ResourceObserver;【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义常量,如果一定要定义,最好确定该常量与接口的方法。相关,并且是整个应用的基础常量。正例:接口方法签名 void commit();接口基础常量 String COMPANY = "alibaba";【参考】枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。说明:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。正例:枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKNOWN_REASON【参考】各层命名规约:A)Service / DAO 层方法命名规约:1)获取单个对象的方法用 get 做前缀。2)获取多个对象的方法用 list 做前缀,复数结尾,如:listObjects3)获取统计值的方法用 count 做前缀。 4)插入的方法用 save / insert 做前缀。5)删除的方法用 remove / delete 做前缀。 6)修改的方法用 update 做前缀。 B)领域模型命名规约: 1)数据对象:xxxDO,xxx 即为数据表名。2)数据传输对象:xxxDTO,xxx 为业务领域相关的名称。3)展示对象:xxxVO,xxx 一般为网页名称。4)POJO 是 DO / DTO / BO / VO 的统称,禁止命名成 xxxPOJO。{mtitle title="常量定义"/}【强制】long 或 Long 赋值时,数值后使用大写 L,不能是小写 l,小写容易跟数字混淆,造成误解。说明:public static final Long NUM = 2l; 写的是数字的 21,还是 Long 型的 2?【强制】浮点数类型的数值后缀统一为大写的 D 或 F。正例:public static final double HEIGHT = 175.5D; public static final float WEIGHT = 150.3F;【推荐】不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。说明:大而全的常量类,杂乱无章,使用查找功能才能定位到要修改的常量,不利于理解,也不利于维护。正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 SystemConfigConsts 下。【推荐】常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量。1)跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下。2)应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下。反例:易懂常量也要统一定义成应用内共享常量,两个程序员在两个类中分别定义了表示“是”的常量: 类 A 中:public static final String YES = "yes";类 B 中:public static final String YES = "y";A.YES.equals(B.YES),预期是 true,但实际返回为 false,导致线上问题。 3)子工程内部共享常量:即在当前子工程的 constant 目录下。4)包内共享常量:即在当前包下单独的 constant 目录下。5)类内共享常量:直接在类内部 private static final 定义。【推荐】如果变量值仅在一个固定范围内变化用 enum 类型来定义。说明:如果存在名称之外的延伸属性应使用 enum 类型,下面正例中的数字就是延伸信息,表示一年中的第几个季节。正例:public enum SeasonEnum { SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4); private int seq; SeasonEnum(int seq) { this.seq = seq; } public int getSeq() { return seq; } }{mtitle title="代码格式"/}【强制】建议每次代码都格式化一下,再提交【强制】注释的双斜线与注释内容之间有且仅有一个空格。正例:// 这是示例注释,请注意在双斜线之后有一个空格【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则: 1)第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例。 2)运算符与下文一起换行。3)方法调用的点符号与下文一起换行。 4)方法调用中的多个参数需要换行时,在逗号后进行。5)在括号前不要换行,见反例。正例:StringBuilder builder = new StringBuilder();// 超过 120 个字符的情况下,换行缩进 4 个空格,并且方法前的点号一起换行builder.append("yang").append("hao")... .append("chen")... .append("chen")... .append("chen");【强制】IDE 的 text file encoding 设置为 UTF-8;IDE 中文件的换行符使用 Unix 格式,不要使用Windows 格式。【推荐】单个方法的总行数不超过 80 行。说明:除注释之外的方法签名、左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不超过 80 行。正例:代码逻辑分清红花和绿叶,个性和共性,绿叶逻辑单独出来成为额外方法,使主干代码更加晰;共性逻辑抽取成为共性方法,便于复用和维护。【推荐】不同逻辑、不同语义、不同业务的代码之间插入一个空行,分隔开来以提升可读性。说明:任何情形,没有必要插入多个空行进行隔开。{mtitle title=" OOP 规约"/}【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。【强制】所有的覆写方法,必须加 @Override 注解。说明:getObject() 与 get0bject() 的问题。一个是字母的 O,一个是数字的 0,加 @Override 可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。【强制】相同参数类型,相同业务含义,才可以使用的可变参数,参数类型避免定义为 Object。说明:可变参数必须放置在参数列表的最后。(建议开发者尽量不用可变参数编程)正例:public List listUsers(String type, Long... ids) {...}【强制】外部正在调用的接口或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加 @Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。【强制】所有整型包装类对象之间值的比较,全部使用 equals 方法比较。说明:对于 Integer var = ? 在 -128 至 127 之间的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。【强制】任何货币金额,均以最小货币单位且为整型类型进行存储。【强制】浮点数之间的等值判断,基本数据类型不能使用 == 进行比较,包装数据类型不能使用 equals进行判断。正例:(1)指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。float a = 1.0F - 0.9F; float b = 0.9F - 0.8F; float diff = 1e-6F; if (Math.abs(a - b) < diff) { System.out.println("true"); }(2)使用 BigDecimal 来定义值,再进行浮点数的运算操作。BigDecimal a = new BigDecimal("1.0"); BigDecimal b = new BigDecimal("0.9"); BigDecimal c = new BigDecimal("0.8"); BigDecimal x = a.subtract(b); BigDecimal y = b.subtract(c); if (x.compareTo(y) == 0) { System.out.println("true"); }【强制】禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象。说明:BigDecimal(double) 存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。如:BigDecimal g = new BigDecimal(0.1F);实际的存储值为:0.100000001490116119384765625正例:优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了 Double 的toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。BigDecimal recommend1 = new BigDecimal("0.1");BigDecimal recommend2 = BigDecimal.valueOf(0.1);【强制】定义 DO / PO / DTO / VO 等 POJO 类时,不要设定任何属性默认值。反例:某业务的 DO 的 createTime 默认值为 new Date();但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。说明:注意 serialVersionUID 不一致会抛出序列化运行时异常。【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。【强制】POJO 类必须写 toString 方法。使用 IDE 中的工具 source > generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString()。说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString() 方法打印其属性值,便于排查问题。【强制】禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法。说明:框架在调用属性 xxx 的提取方法时,并不能确定哪个方法一定是被优先调用到,神坑之一。【推荐】循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。反例:String str = "start"; for (int i = 0; i < 100; i++) { str = str + "hello"; }说明:反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过toString() 返回 String 对象,造成内存资源浪费。【推荐】final 可以声明类、成员变量、方法、以及本地变量,下列情况使用 final 关键字: 1)不允许被继承的类,如:String 类。2)不允许修改引用的域对象,如:POJO 类的域变量。3)不允许被覆写的方法,如:POJO 类的 setter 方法。4)不允许运行过程中重新赋值的局部变量。5)避免上下文重复使用一个变量,使用 final 关键字可以强制重新定义一个变量,方便更好地进行重构。【推荐】类成员与方法访问控制从严: 1)如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。 2)工具类不允许有 public 或 default 构造方法。3)类非 static 成员变量并且与子类共享,必须是 protected。 4)类非 static 成员变量并且仅在本类使用,必须是 private。 5)类 static 成员变量如果仅在本类使用,必须是 private。 6)若是 static 成员变量,考虑是否为 final。 7)类成员方法只供类内部调用,必须是 private。 8)类成员方法只对继承类公开,那么限制为 protected。说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。思考:如果是一个private 的方法,想删除就删除,可是一个 public 的 service 成员方法或成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你会担心的。{mtitle title=" 日期时间"/}【强制】获取当前毫秒数:System.currentTimeMillis();而不是 new Date().getTime()。说明:获取纳秒级时间,则使用 System.nanoTime 的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。【强制】禁止在程序中写死一年为 365 天,避免在公历闰年时出现日期转换错误或程序逻辑错误。正例:// 获取今年的天数 int daysOfThisYear = LocalDate.now().lengthOfYear(); // 获取指定某年的天数 LocalDate.of(2011, 1, 1).lengthOfYear(); {mtitle title="集合处理"/}【强制】关于 hashCode 和 equals 的处理,遵循如下规则: 1)只要覆写 equals,就必须覆写 hashCode。 2)因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两种方法。 3)如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。说明:String 因为覆写了 hashCode 和 equals 方法,所以可以愉快地将 String 对象作为 key 来使用。【强制】判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0 的方式。说明:在某些集合中,前者的时间复杂度为 O(1),而且可读性更好。正例:Map<String, Object> map = new HashMap<>(16); if (map.isEmpty()) { System.out.println("no element in this map."); }【强制】在 subList 场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常。说明:抽查表明,90% 的程序员对此知识点都有错误的认知。【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为0 的空数组。反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现ClassCastException 错误。正例:List<String> list = new ArrayList<>(2); list.add("guan"); list.add("bao"); String[] array = list.toArray(new String[0]);说明:使用 toArray 带参方法,数组空间大小的 length: 1)等于 0,动态创建与 size 相同的数组,性能最好。 2)大于 0 但小于 size,重新创建大小等于 size 的数组,增加 GC 负担。 3)等于 size,在高并发情况下,数组创建完成之后,size 正在变大的情况下,负面影响与 2 相同。 4)大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患【强制】使用 Collection 接口任何实现类的 addAll() 方法时,要对输入的集合参数进行 NPE 判断。说明:在 ArrayList#addAll 方法的第一行代码即 Object[] a = c.toArray();其中 c 为输入集合参数,如果为 null,则直接抛出异常。【强制】使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法,它的 add / remove / clear 方法会抛出 UnsupportedOperationException 异常。说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。String[] str = new String[]{ "yang", "guan", "bao" };List list = Arrays.asList(str);第一种情况:list.add("yangguanbao"); 运行时异常。第二种情况:str[0] = "change"; list 中的元素也会随之修改,反之亦然。【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方法, 而<? super T>不能使用 get 方法,两者在接口调用赋值的场景中容易出错。说明:扩展说一下 PECS(Producer Extends Consumer Super) 原则,即频繁往外读取内容的,适合用<? extends T>,经常往里插入的,适合用<? super T>【强制】在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行instanceof 判断,避免抛出 ClassCastException 异常。说明:毕竟泛型是在 JDK5 后才出现,考虑到向前兼容,编译器是允许非泛型集合与泛型集合互相赋值。.【强制】不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式,如果并发操作,需要对 iterator 对象加锁。正例:List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (删除元素的条件) { iterator.remove(); } }【强制】在 JDK7 版本及以上,Comparator 实现类要满足如下三个条件,不然 Arrays.sort,Collections.sort 会抛 IllegalArgumentException 异常。说明:三个条件如下1)x,y 的比较结果和 y,x 的比较结果相反。2)x > y,y > z,则 x > z。 3)x = y,则 x,z 比较结果和 y,z 比较结果相同。【推荐】泛型集合使用时,在 JDK7 及以上,使用 diamond 语法或全省略。说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。正例:// diamond 方式,即<>HashMap<String, String> userCache = new HashMap<>(16);// 全省略方式ArrayList users = new ArrayList(10);【推荐】集合初始化时,指定集合初始值大小。说明:HashMap 使用构造方法 HashMap(int initialCapacity) 进行初始化时,如果暂时无法确定集合大小,那么指定默认值(16)即可。正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loaderfactor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value。而entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用 Map.forEach 方法。正例:values() 返回的是 V 值集合,是一个 list 集合对象;keySet() 返回的是 K 值集合,是一个 Set 集合对象;entrySet() 返回的是 K-V 值组合的 Set 集合。{mtitle title="并发管理"/}【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。说明:资源驱动类、工具类、单例工厂类都需要注意。【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeatureOfGroup:public class UserThreadFactory implements ThreadFactory { private final String namePrefix; private final AtomicInteger nextId = new AtomicInteger(1); // 定义线程组名称,在利用 jstack 来排查问题时,非常有帮助 UserThreadFactory(String whatFeatureOfGroup) { namePrefix = "FromUserThreadFactory's" + whatFeatureOfGroup + "-Worker-"; } @Override public Thread newThread(Runnable task) { String name = namePrefix + nextId.getAndIncrement(); Thread thread = new Thread(null, task, name, 0, false); System.out.println(thread.getName()); return thread; } }【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors 返回的线程池对象的弊端如下:1)FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。 3)ScheduledThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。正例:注意线程安全,使用 DateUtils。亦推荐如下处理:private static final ThreadLocal<DateFormat> dateStyle = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } };说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。【强制】必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally 块进行回收。正例:objectThreadLocal.set(userInfo); try { // ... } finally { objectThreadLocal.remove(); }【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。【强制】在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。说明一:在 lock 方法与 try 代码块之间的方法调用抛出异常,无法解锁,造成其它线程无法成功获取锁。说明二:如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中,unlock 对未加锁的对象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛出 IllegalMonitorStateException 异常。说明三:在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。正例:Lock lock = new XxxLock(); // ... lock.lock(); try { doSomething(); doOthers(); } finally { lock.unlock(); }【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。说明:Lock 对象的 unlock 方法在执行时,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),如果当前线程不持有锁,则抛出 IllegalMonitorStateException 异常。正例:Lock lock = new XxxLock(); // ... boolean isLocked = lock.tryLock(); if (isLocked) { try { doSomething(); doOthers(); } finally { lock.unlock(); } }【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。【强制】多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。【推荐】资金相关的金融敏感信息,使用悲观锁策略。说明:乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。正例:悲观锁遵循一锁二判三更新四释放的原则。【推荐】使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。说明:注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降。说明:Random 实例包括 java.util.Random 的实例或者 Math.random() 的方式。正例:在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程持有一个单独的 Random 实例。【推荐】通过双重检查锁(double-checked locking),实现延迟初始化需要将目标属性声明为volatile 型,(比如修改 helper 的属性声明为 private volatile Helper helper = null;)。正例:public class LazyInitDemo { private volatile Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized(this) { if (helper == null) { helper = new Helper(); } } } return helper; } // other methods and fields... }【参考】volatile 解决多线程内存不可见问题对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。说明:如果是 count++操作,使用如下类实现:AtomicInteger count = new AtomicInteger();count.addAndGet(1);如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。【参考】HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在开发过程中注意规避此风险。【参考】ThreadLocal 对象使用 static 修饰,ThreadLocal 无法解决共享对象的更新问题。说明:这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。{mtitle title="控制语句"/}【强制】在一个 switch 块内,每个 case 要么通过 continue / break / return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。说明:注意 break 是退出 switch 语句块,而 return 是退出方法体。【强制】在高并发场景中,避免使用“等于”判断作为中断或退出的条件。说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止。【推荐】公开接口需要进行入参保护,尤其是批量操作的接口。反例:某业务系统,提供一个用户批量查询的接口,API 文档上有说最多查多少个,但接口实现上没做任何保护,导致调用方传了一个 1000 的用户 id 数组过来后,查询信息后,内存爆了。{mtitle title="注释规约"/}【强制】类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /* 内容 / 格式,不得使用 // xxx方式。说明:在 IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应注释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。【强制】所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数异常说明外,还必须指出该方法做什么事情,实现什么功能。说明:对子类的实现要求,或者调用注意事项,请一并说明。【强制】所有的类都必须添加创建者和创建日期。说明:在设置模板时,注意 IDEA 的@author 为${USER},而 eclipse 的@author 为${user},大小写有区别,而日期的设置统一为 yyyy/MM/dd 的格式。正例:/** ** @author yangguanbao * @date 2021/11/26 * **/【参考】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。 1)待办事宜(TODO):(标记人,标记时间,[预计处理时间])表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个Javadoc 标签)。2)错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间])在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。{mtitle title="前后端规约"/}【强制】前后端交互的 API,需要明确协议、域名、路径、请求方法、请求内容、状态码、响应体。说明: 1)协议:生产环境必须使用 HTTPS。 2)路径:每一个 API 需对应一个路径,表示 API 具体的请求地址: a)代表一种资源,只能为名词,推荐使用复数,不能为动词,请求方法已经表达动作意义。b)URL 路径不能使用大写,单词如果需要分隔,统一使用下划线。c)路径禁止携带表示请求内容类型的后缀,比如".json",".xml",通过 accept 头表达即可。 3)请求方法:对具体操作的定义,常见的请求方法如下:a)GET:从服务器取出资源。b)POST:在服务器新建一个资源。 c)PUT:在服务器更新资源。d)DELETE:从服务器删除资源。4)请求内容:URL 带的参数必须无敏感信息或符合安全要求;body 里带参数时必须设置 Content-Type。 5)响应体:响应体 body 可放置多种数据类型,由 Content-Type 头来确定。【强制】前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{}。说明:此条约定有利于数据层面上的协作更加高效,减少前端很多琐碎的 null 判断。【强制】服务端发生错误时,返回给前端的响应信息必须包含 HTTP 状态码,errorCode、errorMessage、用户提示信息四个部分。说明:四个部分的涉众对象分别是浏览器、前端开发、错误排查人员、用户。其中输出给用户的提示信息要求:简短清晰、提示友好,引导用户进行下一步操作或解释错误原因,提示信息可以包括错误原因、上下文环境、推荐操作等。errorCode:参考 。errorMessage:简要描述后端出错原因,便于错误排查人员快速定位问题,注意不要包含敏感数据信息。正例:常见的 HTTP 状态码如下1)200 OK:表明该请求被成功地完成,所请求的资源发送到客户端。2)401 Unauthorized:请求要求身份验证,常见对于需要登录而用户未登录的情况。3)403 Forbidden:服务器拒绝请求,常见于机密信息或复制其它登录用户链接访问服务器的情况。4)404 NotFound:服务器无法取得所请求的网页,请求资源不存在。5)500 InternalServerError:服务器内部错误。【强制】在前后端交互的 JSON 格式数据中,所有的 key 必须为小写字母开始的 lowerCamelCase风格,符合英文表达习惯,且表意完整。正例:errorCode / errorMessage / assetStatus / menuList / orderList / configFlag反例:ERRORCODE / ERROR_CODE / error_message / error-message / errormessage【强制】errorMessage 是前后端错误追踪机制的体现,可以在前端输出到 type="hidden" 文字类控件中,或者用户端的日志中,帮助我们快速地定位出问题。【强制】对于需要使用超大整数的场景,服务端一律使用 String 字符串类型返回,禁止使用 Long 类型。说明:Java 服务端如果直接返回 Long 整型数据给前端,Javascript 会自动转换为 Number 类型(注:此类型为双精度浮点数,表示原理与取值范围等同于 Java 中的 Double)。Long 类型能表示的最大值是 263-1,在取值范围之内,超过 253(9007199254740992)的数值转化为 Javascript 的 Number 时,有些数值会产生精度损失。扩展说明,在 Long 取值范围内,任何 2 的指数次的整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题。若浮点数尾数位与指数位空间不限,则可以精确表示任何整数,但很不幸,双精度浮点数的尾数位只有 52 位。反例:通常在订单号或交易号大于等于 16 位,大概率会出现前后端订单数据不一致的情况。比如,后端传输的 "orderId":362909601374617692,前端拿到的值却是:362909601374617660【强制】HTTP 请求通过 URL 传递参数时,不能超过 2048 字节。说明:不同浏览器对于 URL 的最大长度限制略有不同,并且对超出最大长度的处理逻辑也有差异,2048 字节是取所有浏览器的最小值。反例:某业务将退货的商品 id 列表放在 URL 中作为参数传递,当一次退货商品数量过多时,URL 参数超长,传递到后端的参数被截断,导致部分商品未能正确退货。【强制】HTTP 请求通过 body 传递内容时,必须控制长度,超出最大长度后,后端解析会出错。说明:nginx 默认限制是 1MB,tomcat 默认限制为 2MB,当确实有业务需要传较大内容时,可以调大服务器端的限制。【强制】在翻页场景中,用户输入参数的小于 1,则前端返回第一页参数给后端;后端发现用户输入的参数大于总页数,直接返回最后一页。【强制】服务器内部重定向必须使用 forward;外部重定向地址必须使用 URL 统一代理模块生成,否 则会因线上采用 HTTPS 协议而导致浏览器提示“安全”,并且还会带来 URL 维护不一致的问题。【参考】在接口路径中不要加入版本号,版本控制在 HTTP 头信息中体现,有利于向前兼容。说明:当用户在低版本与高版本之间反复切换工作时,会导致迁移复杂度升高,存在数据错乱风险。{mtitle title="其他细节"/}【强制】注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0 ≤ x < 1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法。【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String。说明:如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString() 方法。
2022年04月22日
73 阅读
0 评论
0 点赞
2022-04-22
使用NginxConfig实现Nginx可视化配置
一、NginxConfig简介与安装NginxConfig号称你唯一需要的Nginx配置工具,可以使用可视化界面来生成Nginx配置,功能非常强大,在Github上已有15K+Star!项目地址: 点击进入1.1 安装Node.js由于NginxConfig是一个基于Vue的前端项目,我们首先得安装Node.js。首先从官网下载Node.js的安装包,下载地址:https://nodejs.org/zh-cn/download/下载成功后将安装包解压到/usr/local/src/目录下,使用如下命令即可cd /usr/local/src/ tar xf node-v16.14.2-linux-x64.tar.xz cd node-v16.14.2-linux-x64/ ./bin/node -v使用./bin/node -v命令可查看当前安装版本如果想在Linux命令行中直接运行,还需对node和npm命令创建软链接;ln -s /usr/local/src/node-v16.14.2-linux-x64/bin/node /usr/bin/node ln -s /usr/local/src/node-v16.14.2-linux-x64/bin/npm /usr/bin/npm创建完成后使用命令node-v、npm-v查看版本,至此Node.js安装完成1.2 安装NginxConfig首先下载NginxConfig的安装包,下载地址:https://github.com/digitalocean/nginxconfig.io下载完成后解压到指定目录,并使用npm命令安装依赖并运行tar -zxvf nginxconfig.io-master.tar.gz npm install npm run devNginxConfig运行成功后就可以直接访问了,看下界面支持中文还是挺不错的,访问地址:http://ip:8080二、使用2.1 使用准备首先我们需要安装Nginx我们将实现如下功能,通过静态代理访问在不同目录下的静态网站,通过动态代理来访问SpringBoot提供的API接口# 静态代理,访问文档网站 docs.bdysoft.com # 静态代理,访问前端项目 mall.bdysoft.com # 动态代理,访问线上API api.bdysoft.com需要提前修改下本机host或DNS解析,使得上面的三个域名解析到对应的服务器2.2 文档网站配置(docs.bdysoft.com)在NginxConfig中选择好预设为前端,然后修改服务配置,配置好站点、路径和运行目录;不需要HTTPS的话可以选择不启用;然后在全局配置->安全中去除Content-Security-Policy设置再修改性能配置,开启Gzip压缩,删除资源有效期限制2.2 前端网站配置(mall.bdysoft.com)接下来我们再添加一个站点,修改下服务配置即可,其他和上面的基本一致2.3 API网站配置(Swagger API文档网站的访问 api.bdysoft.com)继续添加一个站点,修改服务配置,只需修改站点名称即可然后启用反向代理并设置,反向代理到线上API路由功能暂时不用可以关闭三、使用配置接下来我们就可以直接下载NginxConfig给我们生成好的配置了我们先来看下NginxConfig给我们生成的配置内容,这种配置手写估计要好一会吧点击按钮下载配置,完成后改个名字,然后上传到Linux服务器的Nginx配置目录下,使用如下命令解压tar -zxvf nginxconfig.io.tar.gz大家可以看到NginxConfig将为我们生成如下配置文件接下来将我们之前的文档网站和前端网站放到Nginx的html目录下,然后重启Nginx就可以查看效果了{dotted startColor="#ff6c6c" endColor="#1989fa"/} 体验了一把NginxConfig的配置生成功能,这种不用手写配置,直接通过可视化界面来生成配置的方式确实很好用。NginxConfig不愧是配置高性能、安全、稳定的NgInx服务器的最简单方法!
2022年04月22日
128 阅读
0 评论
0 点赞
2022-04-22
Java使用注解简单实现敏感数据加解密
在JAVA项目开发中,我们通常需要对敏感字段进行脱敏。import org.apache.commons.lang3.StringUtils; import java.lang.reflect.Field; import java.util.List; /** *描述:获取需要加解密的元素 *@create 2022/4/20 14:58 */ public class BaseInfo implements Cloneable, EncryptDecryptInterface { @Override public <T> T encryptSelf() { toCommaint(this,EncryptFiled.class,"Encrypt"); return (T) this; } @Override public <T> T decryptSelf() { toCommaint(this,DecryptFiled.class,"Decrypt"); return (T) this; } @Override public <T> List<T> encryptSelfList(List<T> l) { for (T t : l){ toCommaint(t,EncryptFiled.class,"Encrypt"); } return l; } @Override public <T> List<T> decryptSelfList(List<T> l) { for (T t : l) { toCommaint(t,DecryptFiled.class,"Decrypt"); } return l; } /** *描述:转换方法 *@create 2022/4/20 16:31 */ public <T> T toCommaint(T t,Class c,String type){ Field[] declaredFields = t.getClass().getDeclaredFields(); try { if (declaredFields != null && declaredFields.length > 0) { for (Field field : declaredFields) { if (field.isAnnotationPresent(c) && field.getType().toString().endsWith("String")) { field.setAccessible(true); String fieldValue = (String) field.get(t); if (StringUtils.isNotEmpty(fieldValue)) { if(type.equals("Decrypt")){ fieldValue= MySqlUtils.decrypt(fieldValue); }else if(type.equals("Encrypt")){ fieldValue= MySqlUtils.encrypt(fieldValue); } field.set(t,fieldValue); } } } } } catch (IllegalAccessException e) { throw new RuntimeException(e); } return t; } }import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** *描述:解密自定义注解 *@create 2022/4/20 14:11 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface DecryptFiled { String value() default ""; }import java.util.List; /** *描述:Base类实现该接口中的自加密自解密方法 *@create 2022/4/20 14:10 */ public interface EncryptDecryptInterface { public <T> T encryptSelf(); public <T> T decryptSelf(); public <T> List<T> encryptSelfList(List<T> c); public <T> List<T> decryptSelfList(List<T> c); }import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** *描述:加密自定义注解 *@create 2022/4/20 14:08 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptFiled { String value() default ""; }import org.springframework.stereotype.Component; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; /** *描述: *@create 2022/4/20 14:49 */ @Component public class MySqlUtils { private static final String KEY_AES = "AES";//加密格式 private static final String key="dongguan_key$123";//秘钥 长度必须是 16 bytes /** *描述:数据加密 *@create 2022/4/20 14:44 */ public static String encrypt(String src) { try { byte[] raw = key.getBytes(); SecretKeySpec skeySpec = new SecretKeySpec(raw, KEY_AES); Cipher cipher = Cipher.getInstance(KEY_AES); cipher.init(Cipher.ENCRYPT_MODE, skeySpec); byte[] encrypted = cipher.doFinal(src.getBytes()); src= byte2hex(encrypted); }catch (Exception e){ System.out.println("加密数据出错="+e); } return src; } /** *描述:数据解密 *@create 2022/4/20 14:45 */ public static String decrypt(String src) { try { byte[] raw = key.getBytes(); SecretKeySpec skeySpec = new SecretKeySpec(raw, KEY_AES); Cipher cipher = Cipher.getInstance(KEY_AES); cipher.init(Cipher.DECRYPT_MODE, skeySpec); byte[] encrypted1 = hex2byte(src); byte[] original = cipher.doFinal(encrypted1); src = new String(original); }catch (Exception e){ System.out.println("加密数据出错="+e); } return src; } public static byte[] hex2byte(String strhex) { if (strhex == null) { return null; } int l = strhex.length(); if (l % 2 == 1) { return null; } byte[] b = new byte[l / 2]; for (int i = 0; i != l / 2; i++) { b[i] = (byte) Integer.parseInt(strhex.substring(i * 2, i * 2 + 2), 16); } return b; } public static String byte2hex(byte[] b) { StringBuilder hs = new StringBuilder(); String stmp = ""; for (int n = 0; n < b.length; n++) { stmp = (Integer.toHexString(b[n] & 0XFF)); if (stmp.length() == 1) { hs.append("0").append(stmp); } else { hs.append(stmp); } } return hs.toString().toUpperCase(); } public static void main(String[] args) throws Exception { String content = "testContext"; System.out.println("原内容 = " + content); String encrypt = MySqlUtils.encrypt(content); System.out.println("加密后 = " + encrypt); String decrypt = MySqlUtils.decrypt(encrypt); System.out.println("解密后 = " + decrypt); } }import com.alibaba.fastjson.JSON; import java.util.ArrayList; import java.util.List; /** *描述:测试 *@create 2022/4/20 14:50 */ public class Test { public static void main(String[] args) { TestPo sd = new TestPo();//要进行加密解密的实体类 sd.setIdcard("6029131988005021537");//注入身份证号 sd.setName("小小"); TestPo sd1 = new TestPo();//要进行加密解密的实体类 sd1.setIdcard("6029131988005021538");//注入身份证号 sd1.setPhone("15919314668"); TestPo sd2 = new TestPo();//要进行加密解密的实体类 sd2.setIdcard("6029131988005021539");//注入身份证号 sd2.setPhone("15919314669"); List<TestPo> lists=new ArrayList<>(); lists.add(sd); lists.add(sd1); lists.add(sd2); List<TestPo> listjm=sd.encryptSelfList(lists); System.out.println("执行自加密后输出:"+JSON.toJSONString(listjm) );//执行自加密后输出 List<TestPo> listem=sd.decryptSelfList(listjm); System.out.println("执行自解密后输出:"+JSON.toJSONString(listem) );//执行自解密后输出 TestPo od1=sd.encryptSelf(); System.out.println("加密"+JSON.toJSONString(od1) );//执行自加密后输出 TestPo od2=od1.decryptSelf(); System.out.println("解密"+JSON.toJSONString(od2));//执行自解密后输出 } }/** *描述:po *@create 2022/4/20 16:21 */ public class TestPo extends BaseInfo { private int id; @DecryptFiled @EncryptFiled private String idcard; @DecryptFiled @EncryptFiled private String phone; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getIdcard() { return idcard; } public void setIdcard(String idcard) { this.idcard = idcard; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
2022年04月22日
103 阅读
0 评论
0 点赞
2022-04-20
Springboot——整合EasyExcel简单读写和文件上传下载
前面的项目一直用的是easypoi来进行excel的上传下载,最近发现阿里的EasyExcel更好用,在这里分享一下,具体的可以在官网探索。官网入口: 点击进入一、添加依赖<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.0.5</version> </dependency>二、测试代码准备首先需要编写一个 数据接收类 、 excel对应类import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class User { private String id; private String name; private String realName; private String sexName; }import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data; @Data public class UserExcel { // index 会影响excel数据排列顺序 @ExcelProperty(value = "编号",index = 1) private String id; @ExcelProperty(value = "别名",index = 2) private String name; @ExcelProperty(value = "名称",index = 4) private String realName; @ExcelProperty(value = "性别",index = 3) private String sexName; }三、示例代码3.1 写文件将数据信息写入 Excel 中:import com.alibaba.excel.EasyExcel; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import pojo.StartApplication; import pojo.User; import pojo.UserExcel; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @SpringBootTest(classes= StartApplication.class) @RunWith(SpringRunner.class) public class TestDemo1 { private List<User> users = new ArrayList<>(); @Before public void before(){ users.add(new User("1","xj1","bunana","1")); users.add(new User("2","xj2","bunana","2")); users.add(new User("3","xj3","bunana","1")); users.add(new User("4","xj4","bunana","1")); } /** * 将数据写入excel * @throws Exception */ @Test public void testWrite() throws Exception { File file = new File("/test/test_w.xlsx"); // 针对文件路径,获取文件所在路径之上的路径信息 if(!file.getParentFile().exists()){ file.getParentFile().mkdirs(); } // 判断文件是否存在 if(!file.exists()){ file.createNewFile(); } EasyExcel.write(file, UserExcel.class).sheet("test").doWrite(users); } }运行测试: 3.2 读文件由于没有excel文件,所以先说的写操作。接下来将写的excel再解析出来。@Test public void testRead() throws Exception { File file = new File("/test/test_w.xlsx"); // 针对文件路径,获取文件所在路径之上的路径信息 if(!file.getParentFile().exists()){ file.getParentFile().mkdirs(); } // 判断文件是否存在 if(!file.exists()){ file.createNewFile(); } InputStream inputStream = new FileInputStream(file); EasyExcel.read(inputStream,UserExcel.class,new AnalysisEventListener<UserExcel>(){ // 解析每条数据时触发 @Override public void invoke(UserExcel userExcel, AnalysisContext analysisContext) { // 如果是需要将数据解析存数据库,建议放集合中再存,不要有一条就存一次 System.out.println(userExcel); } @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { //System.out.println(analysisContext); } // 获取表头数据信息 @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { System.out.println(headMap); } }).sheet("test").doRead(); }四、注意事项4.1 监听器的区别本次读操作,采取read(InputStream inputStream, Class head, ReadListener readListener),配置有数据对应映射处理类。如果使用read(InputStream inputStream, ReadListener readListener)此时监听器invoke(Object 0, AnalysisContext analysisContext)中的Object 是一个linkedhashmap数据类型!4.2 关于UserExcel映射类 属性上增加注解@ExcelProperty可以对数据信息进行配置,这里需要强调一点,index值是从 0 开始算的!!本次这里配置为:则让生成excel文件出现前面空列的问题:其次index值,影响字段在excel表中的顺序!五、关于web的上传和下载 由于上面的读写操作,采取将数据流信息写入文件操作,如果涉及到文件的上传和下载操作,可以直接将流信息填充。如下测试案例所示。 示例代码:import com.alibaba.excel.EasyExcel; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; @Api(value = "EasyExcel文件上传和下载测试类") @RestController public class FileUploadAndDownController { @ApiOperation(value = "Excel文件上传数据解析",notes = "test2") @PostMapping("/upload") public void upload(MultipartFile file) throws IOException { ArrayList<UserExcel> userExcels = new ArrayList<>(); EasyExcel.read(file.getInputStream(),UserExcel.class,new AnalysisEventListener<UserExcel>(){ @Override public void invoke(UserExcel userExcel, AnalysisContext analysisContext) { System.out.println("解析数据:"+userExcel); userExcels.add(userExcel); } @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { } }).sheet("test").doRead(); userExcels.forEach(e->{ System.out.println("存数据库!"); System.out.println(e); }); } @ApiOperation(value = "Excel文件下载",notes = "test2") @GetMapping("download") public void download(HttpServletResponse response) throws Exception { List<User> users = new ArrayList<>(); users.add(new User("1","xj1","bunana","1")); users.add(new User("2","xj2","bunana","2")); users.add(new User("3","xj3","bunana","1")); users.add(new User("4","xj4","bunana","1")); response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系 String fileName = URLEncoder.encode("测试", "UTF-8"); response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); // 绕过了创建临时文件,直接将数据读到流中传递至客户端 EasyExcel.write(response.getOutputStream(), UserExcel.class).sheet("test").doWrite(users); } }上传测试: 下载测试: 链接:http://localhost/download六、文件上传到七牛云有时候我们需要将生成的文件永久性存储,放在服务器显然不合适,可以上传到七牛云。import com.alibaba.excel.EasyExcel; import com.alibaba.excel.support.ExcelTypeEnum; import com.bdysoft.cloud.OSSFactory; import com.bdysoft.common.dto.UploadResultDto; import java.io.ByteArrayOutputStream; import java.util.List; /** * EasyExcel生成文件并上传七牛云 * * @author lvwei */ public class EasyExcelUploadUtil { public static UploadResultDto uploadExcel(Integer storeId, Class<?> obj, List<?> list, String sheetName) { //字节流 ByteArrayOutputStream bos = new ByteArrayOutputStream(); EasyExcel.write(bos, obj) .excelType(ExcelTypeEnum.XLSX) .sheet(sheetName) .doWrite(list); // 调用七牛云的上传方法,上传成功,七牛云会将地址返回 UploadResultDto resultDto = OSSFactory.build(storeId).uploadSuffix(bos.toByteArray(), ExcelTypeEnum.XLSX.getValue()); resultDto.setFileSize(Long.parseLong(bos.size() + "")); return resultDto; } }
2022年04月20日
142 阅读
0 评论
0 点赞
2022-04-18
Nginx配置跨域请求Access-Control-Allow-Origin *
当出现403跨域错误的时候 No 'Access-Control-Allow-Origin' header is present on the requested resource,需要给Nginx服务器配置响应的header参数:一、 解决方案只需要在Nginx的配置文件中配置以下参数:location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; if ($request_method = 'OPTIONS') { return 204; } } 二、 解释Access-Control-Allow-Origin服务器默认是不被允许跨域的。给Nginx服务器配置Access-Control-Allow-Origin *后,表示服务器可以接受所有的请求源(Origin),即接受所有跨域的请求。Access-Control-Allow-Headers 是为了防止出现以下错误:Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.这个错误表示当前请求Content-Type的值不被支持。其实是我们发起了"application/json"的类型请求导致的。这里涉及到一个概念:预检请求(preflight request),请看下面"预检请求"的介绍。Access-Control-Allow-Methods 是为了防止出现以下错误:Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.给OPTIONS 添加 204的返回,是为了处理在发送POST请求时Nginx依然拒绝访问的错误发送"预检请求"时,需要用到方法 OPTIONS ,所以服务器需要允许该方法。三、 预检请求(preflight request) 其实上面的配置涉及到了一个W3C标准:CROS,全称是跨域资源共享 (Cross-origin resource sharing),它的提出就是为了解决跨域请求的。 跨域资源共享(CORS)标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。 其实Content-Type字段的类型为application/json的请求就是上面所说的搭配某些 MIME 类型的 POST 请求,CORS规定,Content-Type不属于以下MIME类型的,都属于预检请求:application/x-www-form-urlencoded multipart/form-data text/plain 所以 application/json的请求 会在正式通信之前,增加一次"预检"请求,这次"预检"请求会带上头部信息 Access-Control-Request-Headers: Content-Type:OPTIONS /api/test HTTP/1.1 Origin: http://foo.example Access-Control-Request-Method: POST Access-Control-Request-Headers: Content-Type ... 省略了一些服务器回应时,返回的头部信息如果不包含Access-Control-Allow-Headers: Content-Type则表示不接受非默认的的Content-Type。即出现以下错误:Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
2022年04月18日
55 阅读
0 评论
0 点赞
2022-04-15
面试官问,MySQL建索引需要遵循哪些原则呢?
1.选择唯一性索引 唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。例如,学生表中学号是具有唯一性的字段。为该字段建立唯一性索引可以很快的确定某个学生的信息。如果使用姓名的话,可能存在同名现象,从而降低查询速度。2.为经常需要排序、分组和联合操作的字段建立索引 经常需要ORDER BY、GROUP BY、DISTINCT和UNION等操作的字段,排序操作会浪费很多时间。如果为其建立索引,可以有效地避免排序操作。3.为常作为查询条件的字段建立索引 如果某个字段经常用来做查询条件,那么该字段的查询速度会影响整个表的查询速度。因此,为这样的字段建立索引,可以提高整个表的查询速度。4.限制索引的数目 索引的数目不是越多越好。每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。修改表时,对索引的重构和更新很麻烦。越多的索引,会使更新表变得很浪费时间。5.尽量使用数据量少的索引 如果索引的值很长,那么查询的速度会受到影响。例如,对一个CHAR(100)类型的字段进行全文检索需要的时间肯定要比对CHAR(10)类型的字段需要的时间要多。6.尽量使用前缀来索引 如果索引字段的值很长,最好使用值的前缀来索引。例如,TEXT和BLOG类型的字段,进行全文检索会很浪费时间。如果只检索字段的前面的若干个字符,这样可以提高检索速度。7.删除不再使用或者很少使用的索引 表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。8.最左前缀匹配原则,非常重要的原则。 mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a 1=”” and=”” b=”2” c=”“> 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。9.=和in可以乱序。 比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式10.尽量选择区分度高的列作为索引。 区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就 是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条 记录11.索引列不能参与计算,保持列“干净”。 比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本 太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’);12.尽量的扩展索引,不要新建索引。 比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可注意:选择索引的最终目的是为了使查询的速度变快。上面给出的原则是最基本的准则,但不能拘泥于上面的准则。读者要在以后的学习和工作中进行不断的实践。根据应用的实际情况进行分析和判断,选择最合适的索引方式。
2022年04月15日
36 阅读
0 评论
0 点赞
2022-04-15
SpringBoot+Redis 搞定搜索栏热搜、不雅文字过滤功能
{mtitle title="简单的热搜功能"/}使用java和redis实现一个简单的热搜功能,具备以下功能:搜索栏展示当前登陆的个人用户的搜索历史记录,删除个人历史记录用户在搜索栏输入某字符,则将该字符记录下来 以zset格式存储的redis中,记录该字符被搜索的个数以及当前的时间戳 (用了DFA算法,感兴趣的自己百度学习吧)每当用户查询了已在redis存在了的字符时,则直接累加个数, 用来获取平台上最热查询的十条数据。(可以自己写接口或者直接在redis中添加一些预备好的关键词)最后还要做不雅文字过滤功能。这个很重要不说了你懂的。代码实现热搜与个人搜索记录功能,主要controller层下几个方法就行了 :向redis 添加热搜词汇(添加的时候使用下面不雅文字过滤的方法来过滤下这个词汇,合法再去存储每次点击给相关词热度 +1根据key搜索相关最热的前十名插入个人搜索记录查询个人搜索记录首先配置好redis数据源等等基础,最后贴上核心的 服务层的代码 :package com.****.****.****.user; import com.bdysoft.service.user.RedisService; import org.apache.commons.lang.StringUtils; import org.springframework.data.redis.core.*; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.*; import java.util.concurrent.TimeUnit; @Transactional @Service("redisService") public class RedisServiceImpl implements RedisService { //导入数据源 @Resource(name = "redisSearchTemplate") private StringRedisTemplate redisSearchTemplate; //新增一条该userid用户在搜索栏的历史记录 //searchkey 代表输入的关键词 @Override public int addSearchHistoryByUserId(String userid, String searchkey) { String shistory = RedisKeyUtils.getSearchHistoryKey(userid); boolean b = redisSearchTemplate.hasKey(shistory); if (b) { Object hk = redisSearchTemplate.opsForHash().get(shistory, searchkey); if (hk != null) { return 1; }else{ redisSearchTemplate.opsForHash().put(shistory, searchkey, "1"); } }else{ redisSearchTemplate.opsForHash().put(shistory, searchkey, "1"); } return 1; } //删除个人历史数据 @Override public Long delSearchHistoryByUserId(String userid, String searchkey) { String shistory = RedisKeyUtils.getSearchHistoryKey(userid); return redisSearchTemplate.opsForHash().delete(shistory, searchkey); } //获取个人历史数据列表 @Override public List<String> getSearchHistoryByUserId(String userid) { List<String> stringList = null; String shistory = RedisKeyUtils.getSearchHistoryKey(userid); boolean b = redisSearchTemplate.hasKey(shistory); if(b){ Cursor<Map.Entry<Object, Object>> cursor = redisSearchTemplate.opsForHash().scan(shistory, ScanOptions.NONE); while (cursor.hasNext()) { Map.Entry<Object, Object> map = cursor.next(); String key = map.getKey().toString(); stringList.add(key); } return stringList; } return null; } //新增一条热词搜索记录,将用户输入的热词存储下来 @Override public int incrementScoreByUserId(String searchkey) { Long now = System.currentTimeMillis(); ZSetOperations zSetOperations = redisSearchTemplate.opsForZSet(); ValueOperations<String, String> valueOperations = redisSearchTemplate.opsForValue(); List<String> title = new ArrayList<>(); title.add(searchkey); for (int i = 0, lengh = title.size(); i < lengh; i++) { String tle = title.get(i); try { if (zSetOperations.score("title", tle) <= 0) { zSetOperations.add("title", tle, 0); valueOperations.set(tle, String.valueOf(now)); } } catch (Exception e) { zSetOperations.add("title", tle, 0); valueOperations.set(tle, String.valueOf(now)); } } return 1; } //根据searchkey搜索其相关最热的前十名 (如果searchkey为null空,则返回redis存储的前十最热词条) @Override public List<String> getHotList(String searchkey) { String key = searchkey; Long now = System.currentTimeMillis(); List<String> result = new ArrayList<>(); ZSetOperations zSetOperations = redisSearchTemplate.opsForZSet(); ValueOperations<String, String> valueOperations = redisSearchTemplate.opsForValue(); Set<String> value = zSetOperations.reverseRangeByScore("title", 0, Double.MAX_VALUE); //key不为空的时候 推荐相关的最热前十名 if(StringUtils.isNotEmpty(searchkey)){ for (String val : value) { if (StringUtils.containsIgnoreCase(val, key)) { if (result.size() > 9) {//只返回最热的前十名 break; } Long time = Long.valueOf(valueOperations.get(val)); if ((now - time) < 2592000000L) {//返回最近一个月的数据 result.add(val); } else {//时间超过一个月没搜索就把这个词热度归0 zSetOperations.add("title", val, 0); } } } }else{ for (String val : value) { if (result.size() > 9) {//只返回最热的前十名 break; } Long time = Long.valueOf(valueOperations.get(val)); if ((now - time) < 2592000000L) {//返回最近一个月的数据 result.add(val); } else {//时间超过一个月没搜索就把这个词热度归0 zSetOperations.add("title", val, 0); } } } return result; } //每次点击给相关词searchkey热度 +1 @Override public int incrementScore(String searchkey) { String key = searchkey; Long now = System.currentTimeMillis(); ZSetOperations zSetOperations = redisSearchTemplate.opsForZSet(); ValueOperations<String, String> valueOperations = redisSearchTemplate.opsForValue(); zSetOperations.incrementScore("title", key, 1); valueOperations.getAndSet(key, String.valueOf(now)); return 1; } }核心的部分写完了,剩下的需要你自己将如上方法融入到你自己的代码中就行了。{mtitle title="代码实现过滤不雅文字"/}在springboot 里面写一个配置类加上@Configuration注解,在项目启动的时候加载一下,代码如下:package com.***.***.interceptor; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import java.io.*; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; //屏蔽敏感词初始化 @Configuration @SuppressWarnings({ "rawtypes", "unchecked" }) public class SensitiveWordInit { // 字符编码 private String ENCODING = "UTF-8"; // 初始化敏感字库 public Map initKeyWord() throws IOException { // 读取敏感词库 ,存入Set中 Set<String> wordSet = readSensitiveWordFile(); // 将敏感词库加入到HashMap中//确定有穷自动机DFA return addSensitiveWordToHashMap(wordSet); } // 读取敏感词库 ,存入HashMap中 private Set<String> readSensitiveWordFile() throws IOException { Set<String> wordSet = null; ClassPathResource classPathResource = new ClassPathResource("static/censorword.txt"); InputStream inputStream = classPathResource.getInputStream(); //敏感词库 try { // 读取文件输入流 InputStreamReader read = new InputStreamReader(inputStream, ENCODING); // 文件是否是文件 和 是否存在 wordSet = new HashSet<String>(); // StringBuffer sb = new StringBuffer(); // BufferedReader是包装类,先把字符读到缓存里,到缓存满了,再读入内存,提高了读的效率。 BufferedReader br = new BufferedReader(read); String txt = null; // 读取文件,将文件内容放入到set中 while ((txt = br.readLine()) != null) { wordSet.add(txt); } br.close(); // 关闭文件流 read.close(); } catch (Exception e) { e.printStackTrace(); } return wordSet; } // 将HashSet中的敏感词,存入HashMap中 private Map addSensitiveWordToHashMap(Set<String> wordSet) { // 初始化敏感词容器,减少扩容操作 Map wordMap = new HashMap(wordSet.size()); for (String word : wordSet) { Map nowMap = wordMap; for (int i = 0; i < word.length(); i++) { // 转换成char型 char keyChar = word.charAt(i); // 获取 Object tempMap = nowMap.get(keyChar); // 如果存在该key,直接赋值 if (tempMap != null) { nowMap = (Map) tempMap; } // 不存在则,则构建一个map,同时将isEnd设置为0,因为他不是最后一个 else { // 设置标志位 Map<String, String> newMap = new HashMap<String, String>(); newMap.put("isEnd", "0"); // 添加到集合 nowMap.put(keyChar, newMap); nowMap = newMap; } // 最后一个 if (i == word.length() - 1) { nowMap.put("isEnd", "1"); } } } return wordMap; } }然后这是工具类代码 :package com.***.***.interceptor; import java.io.IOException; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; //敏感词过滤器:利用DFA算法 进行敏感词过滤 public class SensitiveFilter { //敏感词过滤器:利用DFA算法 进行敏感词过滤 private Map sensitiveWordMap = null; // 最小匹配规则 public static int minMatchType = 1; // 最大匹配规则 public static int maxMatchType = 2; // 单例 private static SensitiveFilter instance = null; // 构造函数,初始化敏感词库 private SensitiveFilter() throws IOException { sensitiveWordMap = new SensitiveWordInit().initKeyWord(); } // 获取单例 public static SensitiveFilter getInstance() throws IOException { if (null == instance) { instance = new SensitiveFilter(); } return instance; } // 获取文字中的敏感词 public Set<String> getSensitiveWord(String txt, int matchType) { Set<String> sensitiveWordList = new HashSet<String>(); for (int i = 0; i < txt.length(); i++) { // 判断是否包含敏感字符 int length = CheckSensitiveWord(txt, i, matchType); // 存在,加入list中 if (length > 0) { sensitiveWordList.add(txt.substring(i, i + length)); // 减1的原因,是因为for会自增 i = i + length - 1; } } return sensitiveWordList; } // 替换敏感字字符 public String replaceSensitiveWord(String txt, int matchType, String replaceChar) { String resultTxt = txt; // 获取所有的敏感词 Set<String> set = getSensitiveWord(txt, matchType); Iterator<String> iterator = set.iterator(); String word = null; String replaceString = null; while (iterator.hasNext()) { word = iterator.next(); replaceString = getReplaceChars(replaceChar, word.length()); resultTxt = resultTxt.replaceAll(word, replaceString); } return resultTxt; } /** * 获取替换字符串 * * @param replaceChar * @param length * @return */ private String getReplaceChars(String replaceChar, int length) { String resultReplace = replaceChar; for (int i = 1; i < length; i++) { resultReplace += replaceChar; } return resultReplace; } /** * 检查文字中是否包含敏感字符,检查规则如下:<br> * 如果存在,则返回敏感词字符的长度,不存在返回0 * @param txt * @param beginIndex * @param matchType * @return */ public int CheckSensitiveWord(String txt, int beginIndex, int matchType) { // 敏感词结束标识位:用于敏感词只有1位的情况 boolean flag = false; // 匹配标识数默认为0 int matchFlag = 0; Map nowMap = sensitiveWordMap; for (int i = beginIndex; i < txt.length(); i++) { char word = txt.charAt(i); // 获取指定key nowMap = (Map) nowMap.get(word); // 存在,则判断是否为最后一个 if (nowMap != null) { // 找到相应key,匹配标识+1 matchFlag++; // 如果为最后一个匹配规则,结束循环,返回匹配标识数 if ("1".equals(nowMap.get("isEnd"))) { // 结束标志位为true flag = true; // 最小规则,直接返回,最大规则还需继续查找 if (SensitiveFilter.minMatchType == matchType) { break; } } } // 不存在,直接返回 else { break; } } if (SensitiveFilter.maxMatchType == matchType){ if(matchFlag < 2 || !flag){ //长度必须大于等于1,为词 matchFlag = 0; } } if (SensitiveFilter.minMatchType == matchType){ if(matchFlag < 2 && !flag){ //长度必须大于等于1,为词 matchFlag = 0; } } return matchFlag; } }在你代码的controller层直接调用方法判断即可://非法敏感词汇判断 SensitiveFilter filter = SensitiveFilter.getInstance(); int n = filter.CheckSensitiveWord(searchkey,0,1); if(n > 0){ //存在非法字符 logger.info("这个人输入了非法字符--> {},不知道他到底要查什么~ userid--> {}",searchkey,userid); return null; }也可将敏感文字替换*等字符 : SensitiveFilter filter = SensitiveFilter.getInstance(); String text = "敏感文字"; String x = filter.replaceSensitiveWord(text, 1, "*");最后刚才的 SensitiveWordInit.java 里面用到了 censorword.text 文件,放到你项目里面的 resources 目录下的 static 目录中,这个文件就是不雅文字大全,也需要您与时俱进的更新,项目启动的时候会加载该文件。
2022年04月15日
29 阅读
0 评论
0 点赞
2022-04-15
设计模式之代理模式
代理模式是一种结构型设计模式。结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。结构型模式包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。代理模式的应用场景业务系统的非功能性需求开发。比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类中统一处理,让程序员只需要关注业务方面的开发。RPC、缓存中应用。RPC 框架也可以看作一种代理模式;假设我们要开发一个接口请求的缓存功能,对于某些接口请求,如果入参相同,在设定的过期时间内,直接返回缓存结果,而不用重新进行逻辑处理。 代理模式分为 静态代理 和 动态代理 。静态代理的代理对象,在程序编译时已经写好 Java 文件了,直接 new 一个代理对象即可。动态代理产生代理对象的时机是运行时动态生成,它没有 Java 源文件,直接生成字节码文件实例化代理对象。静态代理的实现有两种:通过接口和通过继承。静态代理 - 通过接口方式类图 代码实现public interface UserService { public Boolean login(String username, String password); } UserServiceImpl public class UserServiceImpl implements UserService { @Override public Boolean login(String username, String password) { return "admin".equals(username) && "admin".equals(password); } }public class UserServiceProxy implements UserService { private final UserService userService; public UserServiceProxy(UserService userService) { this.userService = userService; } @Override public Boolean login(String username, String password) { long t1 = System.nanoTime(); System.out.println("start login"); Boolean result = userService.login(username, password); System.out.println("end login"); long t2 = System.nanoTime(); System.out.println("time: " + (t2 - t1) + "ns"); return result; } }public class Main { public static void main(String[] args) { UserService userService = new UserServiceProxy(new UserServiceImpl()); Boolean result = userService.login("admin", "admin"); System.out.println("result: " + result); } }静态代理 - 通过继承方式 类图 public interface UserService { public Boolean login(String username, String password); }public class UserServiceImpl implements UserService { @Override public Boolean login(String username, String password) { return "admin".equals(username) && "admin".equals(password); } }public class UserServiceProxy extends UserServiceImpl { @Override public Boolean login(String username, String password) { long t1 = System.nanoTime(); System.out.println("start login"); Boolean result = super.login(username, password); System.out.println("end login"); long t2 = System.nanoTime(); System.out.println("time: " + (t2 - t1) + "ns"); return result; } }public class Main { public static void main(String[] args) { UserService userService = new UserServiceProxy(); Boolean result = userService.login("admin", "admin"); System.out.println("result: " + result); } }动态代理上面的代码实现有两个问题:我们需要在代理类中,将原始类中的所有的方法,都重新实现一遍,并且为每个方法都附加相似的代码逻辑。如果要添加的附加功能的类有不止一个,我们需要针对每个类都创建一个代理类。 这时我们就需要用到动态代理(Dynamic Proxy),就是我们不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。如何实现动态代理呢? Java 语言本身就已经提供了动态代理的语法(实际上,动态代理底层依赖的就是 Java 的反射语法)。类图 public interface UserService { public Boolean login(String username, String password);public class UserServiceImpl implements UserService { @Override public Boolean login(String username, String password) { return "admin".equals(username) && "admin".equals(password); } }public class DynamicProxyHandler implements InvocationHandler { private Object target; public DynamicProxyHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long t1 = System.nanoTime(); System.out.println("start " + target.getClass().getName() + ":" + method.getName()); Object result = method.invoke(target, args); System.out.println("end " + target.getClass().getName() + ":" + method.getName()); long t2 = System.nanoTime(); System.out.println("time: " + (t2 - t1) + "ns"); return result; } }public class DynamicProxy { public Object createProxy(Object target) { ClassLoader classLoader = target.getClass().getClassLoader(); Class<?>[] interfaces = target.getClass().getInterfaces(); DynamicProxyHandler handler = new DynamicProxyHandler(target); return Proxy.newProxyInstance(classLoader, interfaces, handler); } }public class Main { public static void main(String[] args) { DynamicProxy proxy = new DynamicProxy(); UserService userService = (UserService) proxy.createProxy(new UserServiceImpl()); Boolean result = userService.login("admin", "admin"); System.out.println("result: " + result); } }至此,我们已经实现了基于JDK的动态代理,JDK动态代理要求要代理的类必须实现接口,如果类没有实现接口,那么你可以尝试 CGLIB,它不是JDK自带的,而是第三方类库,感兴趣可以详细了解。
2022年04月15日
62 阅读
0 评论
0 点赞
2022-04-14
开发从未如此简单,SpringBoot学习
0.学习目标了解SpringBoot的作用掌握java配置的方式了解SpringBoot自动配置原理掌握SpringBoot的基本使用了解Thymeleaf的基本使用1. 了解SpringBoot在这一部分,我们主要了解以下3个问题:什么是SpringBoot为什么要学习SpringBootSpringBoot的特点1.1.什么是SpringBootSpringBoot是Spring项目中的一个子工程,与我们所熟知的Spring-framework 同属于spring的产品:我们可以看到下面的一段介绍:Takes an opinionated view of building production-ready Spring applications. Spring Boot favors convention over configuration and is designed to get you up and running as quickly as possible.翻译一下:用一些固定的方式来构建生产级别的spring应用。Spring Boot 推崇约定大于配置的方式以便于你能够尽可能快速的启动并运行程序。其实人们把Spring Boot 称为搭建程序的脚手架。其最主要作用就是帮我们快速的构建庞大的spring项目,并且尽可能的减少一切xml配置,做到开箱即用,迅速上手,让我们关注与业务而非配置。1.2.为什么要学习SpringBootjava一直被人诟病的一点就是臃肿、麻烦。当我们还在辛苦的搭建项目时,可能Python程序员已经把功能写好了,究其原因注意是两点:复杂的配置,项目各种配置其实是开发时的损耗, 因为在思考 Spring 特性配置和解决业务问题之间需要进行思维切换,所以写配置挤占了写应用程序逻辑的时间。一个是混乱的依赖管理。项目的依赖管理也是件吃力不讨好的事情。决定项目里要用哪些库就已经够让人头痛的了,你还要知道这些库的哪个版本和其他库不会有冲突,这难题实在太棘手。并且,依赖管理也是一种损耗,添加依赖不是写应用程序代码。一旦选错了依赖的版本,随之而来的不兼容问题毫无疑问会是生产力杀手。而SpringBoot让这一切成为过去!Spring Boot 简化了基于Spring的应用开发,只需要“run”就能创建一个独立的、生产级别的Spring应用。Spring Boot为Spring平台及第三方库提供开箱即用的设置(提供默认设置,存放默认配置的包就是启动器),这样我们就可以简单的开始。多数Spring Boot应用只需要很少的Spring配置。我们可以使用SpringBoot创建java应用,并使用java –jar 启动它,就能得到一个生产级别的web工程。1.3.SpringBoot的特点Spring Boot 主要目标是:为所有 Spring 的开发者提供一个非常快速的、广泛接受的入门体验开箱即用(启动器starter-其实就是SpringBoot提供的一个jar包),但通过自己设置参数(.properties),即可快速摆脱这种方式。提供了一些大型项目中常见的非功能性特性,如内嵌服务器、安全、指标,健康检测、外部化配置等绝对没有代码生成,也无需 XML 配置。更多细节,大家可以到官网查看。2.快速入门接下来,我们就来利用SpringBoot搭建一个web工程,体会一下SpringBoot的魅力所在!2.1.创建工程我们先新建一个空的工程:工程名为demo:新建一个model:使用maven来构建:然后填写项目坐标:目录结构:项目结构:2.2.添加依赖看到这里很多同学会有疑惑,前面说传统开发的问题之一就是依赖管理混乱,怎么这里我们还需要管理依赖呢?难道SpringBoot不帮我们管理吗?别着急,现在我们的项目与SpringBoot还没有什么关联。SpringBoot提供了一个名为spring-boot-starter-parent的工程,里面已经对各种常用依赖(并非全部)的版本进行了管理,我们的项目需要以这个项目为父工程,这样我们就不用操心依赖的版本问题了,需要什么依赖,直接引入坐标即可!2.2.1.添加父工程坐标 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent>2.2.2.添加web启动器为了让SpringBoot帮我们完成各种自动配置,我们必须引入SpringBoot提供的自动配置依赖,我们称为启动器。因为我们是web项目,这里我们引入web启动器: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>需要注意的是,我们并没有在这里指定版本信息。因为SpringBoot的父工程已经对版本进行了管理了。这个时候,我们会发现项目中多出了大量的依赖:这些都是SpringBoot根据spring-boot-starter-web这个依赖自动引入的,而且所有的版本都已经管理好,不会出现冲突。2.2.3.管理jdk版本默认情况下,maven工程的jdk版本是1.5,而我们开发使用的是1.8,因此这里我们需要修改jdk版本,只需要简单的添加以下属性即可: <properties> <java.version>1.8</java.version> </properties>2.2.4.完整pom<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.leyou.demo</groupId> <artifactId>springboot-demo</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>1.8</java.version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project>2.3.启动类Spring Boot项目通过main函数即可启动,我们需要创建一个启动类:然后编写main函数:@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }2.4.编写controller接下来,我们就可以像以前那样开发SpringMVC的项目了!我们编写一个controller:代码:@RestController public class HelloController { @GetMapping("hello") public String hello(){ return "hello, spring boot!"; } } 2.5.启动测试接下来,我们运行main函数,查看控制台:并且可以看到监听的端口信息:1)监听的端口是80802)SpringMVC的映射路径是:/3)/hello路径已经映射到了HelloController中的hello()方法打开页面访问:http://localhost:8080/hello测试成功了!3.Java配置在入门案例中,我们没有任何的配置,就可以实现一个SpringMVC的项目了,快速、高效!但是有同学会有疑问,如果没有任何的xml,那么我们如果要配置一个Bean该怎么办?比如我们要配置一个数据库连接池,以前会这么玩:<!-- 配置连接池 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean>现在该怎么做呢?3.1.回顾历史事实上,在Spring3.0开始,Spring官方就已经开始推荐使用java配置来代替传统的xml配置了,我们不妨来回顾一下Spring的历史:Spring1.0时代在此时因为jdk1.5刚刚出来,注解开发并未盛行,因此一切Spring配置都是xml格式,想象一下所有的bean都用xml配置,细思极恐啊,心疼那个时候的程序员2秒Spring2.0时代Spring引入了注解开发,但是因为并不完善,因此并未完全替代xml,此时的程序员往往是把xml与注解进行结合,貌似我们之前都是这种方式。Spring3.0及以后3.0以后Spring的注解已经非常完善了,因此Spring推荐大家使用完全的java配置来代替以前的xml,不过似乎在国内并未推广盛行。然后当SpringBoot来临,人们才慢慢认识到java配置的优雅。有句古话说的好:拥抱变化,拥抱未来。所以我们也应该顺应时代潮流,做时尚的弄潮儿,一起来学习下java配置的玩法。3.2.尝试java配置java配置主要靠java类和一些注解,比较常用的注解有:@Configuration:声明一个类作为配置类,代替xml文件@Bean:声明在方法上,将方法的返回值加入Bean容器,代替<bean>标签@value:属性注入@PropertySource:指定外部属性文件,我们接下来用java配置来尝试实现连接池配置:首先引入Druid连接池依赖:<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.6</version> </dependency>创建一个jdbc.properties文件,编写jdbc属性:jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/leyou jdbc.username=root jdbc.password=123然后编写代码:@Configuration @PropertySource("classpath:jdbc.properties") public class JdbcConfig { @Value("${jdbc.url}") String url; @Value("${jdbc.driverClassName}") String driverClassName; @Value("${jdbc.username}") String username; @Value("${jdbc.password}") String password; @Bean public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(url); dataSource.setDriverClassName(driverClassName); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } }解读:@Configuration:声明我们JdbcConfig是一个配置类@PropertySource:指定属性文件的路径是:classpath:jdbc.properties通过@Value为属性注入值通过@Bean将 dataSource()方法声明为一个注册Bean的方法,Spring会自动调用该方法,将方法的返回值加入Spring容器中。然后我们就可以在任意位置通过@Autowired注入DataSource了!我们在HelloController中测试:@RestController public class HelloController { @Autowired private DataSource dataSource; @GetMapping("hello") public String hello() { return "hello, spring boot!" + dataSource; } }然后Debug运行并查看:属性注入成功了!3.3.SpringBoot的属性注入在上面的案例中,我们实验了java配置方式。不过属性注入使用的是@Value注解。这种方式虽然可行,但是不够强大,因为它只能注入基本类型值。在SpringBoot中,提供了一种新的属性注入方式,支持各种java基本数据类型及复杂类型的注入。1)我们新建一个类,用来进行属性注入:@ConfigurationProperties(prefix = "jdbc") public class JdbcProperties { private String url; private String driverClassName; private String username; private String password; // ... 略 // getters 和 setters } 在类上通过@ConfigurationProperties注解声明当前类为属性读取类prefix="jdbc"读取属性文件中,前缀为jdbc的值。在类上定义各个属性,名称必须与属性文件中jdbc.后面部分一致需要注意的是,这里我们并没有指定属性文件的地址,所以我们需要把jdbc.properties名称改为application.properties,这是SpringBoot默认读取的属性文件名:2)在JdbcConfig中使用这个属性:@Configuration @EnableConfigurationProperties(JdbcProperties.class) public class JdbcConfig { @Bean public DataSource dataSource(JdbcProperties jdbc) { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(jdbc.getUrl()); dataSource.setDriverClassName(jdbc.getDriverClassName()); dataSource.setUsername(jdbc.getUsername()); dataSource.setPassword(jdbc.getPassword()); return dataSource; } }通过@EnableConfigurationProperties(JdbcProperties.class)来声明要使用JdbcProperties这个类的对象然后你可以通过以下方式注入JdbcProperties:@Autowired注入@Autowired private JdbcProperties prop;构造函数注入private JdbcProperties prop; public JdbcConfig(Jdbcproperties prop){ this.prop = prop; }声明有@Bean的方法参数注入@Bean public Datasource dataSource(JdbcProperties prop){ // ... }本例中,我们采用第三种方式。3)测试结果:大家会觉得这种方式似乎更麻烦了,事实上这种方式有更强大的功能,也是SpringBoot推荐的注入方式。两者对比关系:优势:Relaxed binding:松散绑定不严格要求属性文件中的属性名与成员变量名一致。支持驼峰,中划线,下划线等等转换,甚至支持对象引导。比如:user.friend.name:代表的是user对象中的friend属性中的name属性,显然friend也是对象。@value注解就难以完成这样的注入方式。meta-data support:元数据支持,帮助IDE生成属性提示(写开源框架会用到)。3.4、更优雅的注入事实上,如果一段属性只有一个Bean需要使用,我们无需将其注入到一个类(JdbcProperties)中。而是直接在需要的地方声明即可:@Configuration public class JdbcConfig { @Bean // 声明要注入的属性前缀,SpringBoot会自动把相关属性通过set方法注入到DataSource中 @ConfigurationProperties(prefix = "jdbc") public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); return dataSource; } }我们直接把@ConfigurationProperties(prefix = "jdbc")声明在需要使用的@Bean的方法上,然后SpringBoot就会自动调用这个Bean(此处是DataSource)的set方法,然后完成注入。使用的前提是:该类必须有对应属性的set方法!我们将jdbc的url改成:/heima,再次测试:吾爱程序猿(www.52programer.com)打造专业优质的IT教程分享社区4.自动配置原理使用SpringBoot之后,一个整合了SpringMVC的WEB工程开发,变的无比简单,那些繁杂的配置都消失不见了,这是如何做到的?一切魔力的开始,都是从我们的main函数来的,所以我们再次来看下启动类:我们发现特别的地方有两个:注解:@SpringBootApplicationrun方法:SpringApplication.run()我们分别来研究这两个部分。4.1.了解@SpringBootApplication点击进入,查看源码:这里重点的注解有3个:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan4.1.1.@SpringBootConfiguration我们继续点击查看源码:通过这段我们可以看出,在这个注解上面,又有一个@Configuration注解。通过上面的注释阅读我们知道:这个注解的作用就是声明当前类是一个配置类,然后Spring会自动扫描到添加了@Configuration的类,并且读取其中的配置信息。而@SpringBootConfiguration是来声明当前类是SpringBoot应用的配置类,项目中只能有一个。所以一般我们无需自己添加。4.1.2.@EnableAutoConfiguration关于这个注解,官网上有一段说明:The second class-level annotation is @EnableAutoConfiguration. This annotationtells Spring Boot to “guess” how you want to configure Spring, based on the jardependencies that you have added. Since spring-boot-starter-web added Tomcatand Spring MVC, the auto-configuration assumes that you are developing a webapplication and sets up Spring accordingly.简单翻译以下:第二级的注解@EnableAutoConfiguration,告诉SpringBoot基于你所添加的依赖,去“猜测”你想要如何配置Spring。比如我们引入了spring-boot-starter-web,而这个启动器中帮我们添加了tomcat、SpringMVC的依赖。此时自动配置就知道你是要开发一个web应用,所以就帮你完成了web及SpringMVC的默认配置了!总结,SpringBoot内部对大量的第三方库或Spring内部库进行了默认配置,这些配置是否生效,取决于我们是否引入了对应库所需的依赖,如果有那么默认配置就会生效。所以,我们使用SpringBoot构建一个项目,只需要引入所需框架的依赖,配置就可以交给SpringBoot处理了。除非你不希望使用SpringBoot的默认配置,它也提供了自定义配置的入口。4.1.3.@ComponentScan我们跟进源码:并没有看到什么特殊的地方。我们查看注释:大概的意思:配置组件扫描的指令。提供了类似与<context:component-scan>标签的作用通过basePackageClasses或者basePackages属性来指定要扫描的包。如果没有指定这些属性,那么将从声明这个注解的类所在的包开始,扫描包及子包而我们的@SpringBootApplication注解声明的类就是main函数所在的启动类,因此扫描的包是该类所在包及其子包。因此,一般启动类会放在一个比较前的包目录中。4.2.默认配置原理4.2.1默认配置类通过刚才的学习,我们知道@EnableAutoConfiguration会开启SpringBoot的自动配置,并且根据你引入的依赖来生效对应的默认配置。那么问题来了:这些默认配置是在哪里定义的呢?为何依赖引入就会触发配置呢?其实在我们的项目中,已经引入了一个依赖:spring-boot-autoconfigure,其中定义了大量自动配置类:还有:非常多,几乎涵盖了现在主流的开源框架,例如:redisjmsamqpjdbcjacksonmongodbjpasolrelasticsearch... 等等我们来看一个我们熟悉的,例如SpringMVC,查看mvc 的自动配置类:打开WebMvcAutoConfiguration:我们看到这个类上的4个注解:@Configuration:声明这个类是一个配置类@ConditionalOnWebApplication(type = Type.SERVLET)ConditionalOn,翻译就是在某个条件下,此处就是满足项目的类是是Type.SERVLET类型,也就是一个普通web工程,显然我们就是@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })这里的条件是OnClass,也就是满足以下类存在:Servlet、DispatcherServlet、WebMvcConfigurer,其中Servlet只要引入了tomcat依赖自然会有,后两个需要引入SpringMVC才会有。这里就是判断你是否引入了相关依赖,引入依赖后该条件成立,当前类的配置才会生效!@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)这个条件与上面不同,OnMissingBean,是说环境中没有指定的Bean这个才生效。其实这就是自定义配置的入口,也就是说,如果我们自己配置了一个WebMVCConfigurationSupport的类,那么这个默认配置就会失效!接着,我们查看该类中定义了什么:视图解析器:处理器适配器(HandlerAdapter):还有很多,这里就不一一截图了。4.2.2.默认配置属性另外,这些默认配置的属性来自哪里呢?我们看到,这里通过@EnableAutoConfiguration注解引入了两个属性:WebMvcProperties和ResourceProperties。这不正是SpringBoot的属性注入玩法嘛。我们查看这两个属性类:找到了内部资源视图解析器的prefix和suffix属性。ResourceProperties中主要定义了静态资源(.js,.html,.css等)的路径:如果我们要覆盖这些默认属性,只需要在application.properties中定义与其前缀prefix和字段名一致的属性即可。4.3.总结SpringBoot为我们提供了默认配置,而默认配置生效的条件一般有两个:你引入了相关依赖你自己没有配置1)启动器所以,我们如果不想配置,只需要引入依赖即可,而依赖版本我们也不用操心,因为只要引入了SpringBoot提供的stater(启动器),就会自动管理依赖及版本了。因此,玩SpringBoot的第一件事情,就是找启动器,SpringBoot提供了大量的默认启动器,参考课前资料中提供的《SpringBoot启动器.txt》2)全局配置另外,SpringBoot的默认配置,都会读取默认属性,而这些属性可以通过自定义application.properties文件来进行覆盖。这样虽然使用的还是默认配置,但是配置中的值改成了我们自定义的。因此,玩SpringBoot的第二件事情,就是通过application.properties来覆盖默认属性值,形成自定义配置。我们需要知道SpringBoot的默认属性key,非常多,参考课前资料提供的:《SpringBoot全局属性.md》吾爱程序猿(www.52programer.com)打造专业优质的IT教程分享社区5.SpringBoot实践接下来,我们来看看如何用SpringBoot来玩转以前的SSM,我们沿用之前讲解SSM用到的数据库tb_user和实体类User5.1.整合SpringMVC虽然默认配置已经可以使用SpringMVC了,不过我们有时候需要进行自定义配置。5.1.1.修改端口查看SpringBoot的全局属性可知,端口通过以下方式配置:# 映射端口 server.port=80重启服务后测试:5.1.2.访问静态资源现在,我们的项目是一个jar工程,那么就没有webapp,我们的静态资源该放哪里呢?回顾我们上面看的源码,有一个叫做ResourceProperties的类,里面就定义了静态资源的默认查找路径:默认的静态资源路径为:classpath:/META-INF/resources/classpath:/resources/classpath:/static/classpath:/public只要静态资源放在这些目录中任何一个,SpringMVC都会帮我们处理。我们习惯会把静态资源放在classpath:/static/目录下。我们创建目录,并且添加一些静态资源:重启项目后测试:5.1.3.添加拦截器拦截器也是我们经常需要使用的,在SpringBoot中该如何配置呢?拦截器不是一个普通属性,而是一个类,所以就要用到java配置方式了。在SpringBoot官方文档中有这么一段说明:If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.翻译:如果你想要保持Spring Boot 的一些默认MVC特征,同时又想自定义一些MVC配置(包括:拦截器,格式化器, 视图控制器、消息转换器 等等),你应该让一个类实现WebMvcConfigurer,并且添加@Configuration注解,但是千万不要加@EnableWebMvc注解。如果你想要自定义HandlerMapping、HandlerAdapter、ExceptionResolver等组件,你可以创建一个WebMvcRegistrationsAdapter实例 来提供以上组件。如果你想要完全自定义SpringMVC,不保留SpringBoot提供的一切特征,你可以自己定义类并且添加@Configuration注解和@EnableWebMvc注解总结:通过实现WebMvcConfigurer并添加@Configuration注解来实现自定义部分SpringMvc配置。首先我们定义一个拦截器:public class LoginInterceptor implements HandlerInterceptor { private Logger logger = LoggerFactory.getLogger(LoginInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { logger.debug("preHandle method is now running!"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { logger.debug("postHandle method is now running!"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { logger.debug("afterCompletion method is now running!"); } }然后,我们定义配置类,注册拦截器:@Configuration public class MvcConfig implements WebMvcConfigurer{ /** * 通过@Bean注解,将我们定义的拦截器注册到Spring容器 * @return */ @Bean public LoginInterceptor loginInterceptor(){ return new LoginInterceptor(); } /** * 重写接口中的addInterceptors方法,添加自定义拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { // 通过registry来注册拦截器,通过addPathPatterns来添加拦截路径 registry.addInterceptor(this.loginInterceptor()).addPathPatterns("/**"); } }结构如下:接下来运行并查看日志:你会发现日志中什么都没有,因为我们记录的log级别是debug,默认是显示info以上,我们需要进行配置。SpringBoot通过logging.level.*=debug来配置日志级别,*填写包名# 设置com.leyou包的日志级别为debug logging.level.com.leyou=debug再次运行查看:2018-05-05 17:50:01.811 DEBUG 4548 --- [p-nio-80-exec-1] com.leyou.interceptor.LoginInterceptor : preHandle method is now running! 2018-05-05 17:50:01.854 DEBUG 4548 --- [p-nio-80-exec-1] com.leyou.interceptor.LoginInterceptor : postHandle method is now running! 2018-05-05 17:50:01.854 DEBUG 4548 --- [p-nio-80-exec-1] com.leyou.interceptor.LoginInterceptor : afterCompletion method is now running!5.2.整合jdbc和事务spring中的jdbc连接和事务是配置中的重要一环,在SpringBoot中该如何处理呢?答案是不需要处理,我们只要找到SpringBoot提供的启动器即可:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>当然,不要忘了数据库驱动,SpringBoot并不知道我们用的什么数据库,这里我们选择MySQL:<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>至于事务,SpringBoot中通过注解来控制。就是我们熟知的@Transactional@Service public class UserService { @Autowired private UserMapper userMapper; public User queryById(Long id){ return this.userMapper.selectByPrimaryKey(id); } @Transactional public void deleteById(Long id){ this.userMapper.deleteByPrimaryKey(id); } }5.3.整合连接池其实,在刚才引入jdbc启动器的时候,SpringBoot已经自动帮我们引入了一个连接池:HikariCP应该是目前速度最快的连接池了,我们看看它与c3p0的对比:因此,我们只需要指定连接池参数即可:# 连接四大参数 spring.datasource.url=jdbc:mysql://localhost:3306/heima spring.datasource.username=root spring.datasource.password=123 # 可省略,SpringBoot自动推断 spring.datasource.driverClassName=com.mysql.jdbc.Driver spring.datasource.hikari.idle-timeout=60000 spring.datasource.hikari.maximum-pool-size=30 spring.datasource.hikari.minimum-idle=10当然,如果你更喜欢Druid连接池,也可以使用Druid官方提供的启动器:<!-- Druid连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.6</version> </dependency>而连接信息的配置与上面是类似的,只不过在连接池特有属性上,方式略有不同:#初始化连接数 spring.datasource.druid.initial-size=1 #最小空闲连接 spring.datasource.druid.min-idle=1 #最大活动连接 spring.datasource.druid.max-active=20 #获取连接时测试是否可用 spring.datasource.druid.test-on-borrow=true #监控页面启动 spring.datasource.druid.stat-view-servlet.allow=true 5.4.整合mybatis5.4.1.mybatisSpringBoot官方并没有提供Mybatis的启动器,不过Mybatis官网自己实现了:<!--mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> 配置,基本没有需要配置的:# mybatis 别名扫描 mybatis.type-aliases-package=com.heima.pojo # mapper.xml文件位置,如果没有映射文件,请注释掉 mybatis.mapper-locations=classpath:mappers/*.xml需要注意,这里没有配置mapper接口扫描包,因此我们需要给每一个Mapper接口添加@Mapper注解,才能被识别。@Mapper public interface UserMapper { }5.4.2.通用mapper通用Mapper的作者也为自己的插件编写了启动器,我们直接引入即可:<!-- 通用mapper --> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.0.2</version> </dependency>不需要做任何配置就可以使用了。@Mapper public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User>{ }5.5.启动测试将controller进行简单改造:@RestController public class HelloController { @Autowired private UserService userService; @GetMapping("/hello") public User hello() { User user = this.userService.queryById(8L); return user; } } 我们启动项目,查看:吾爱程序猿(www.52programer.com)打造专业优质的IT教程分享社区6.Thymeleaf快速入门SpringBoot并不推荐使用jsp,但是支持一些模板引擎技术:以前大家用的比较多的是Freemarker,但是我们今天的主角是Thymeleaf!6.1.为什么是Thymeleaf?简单说, Thymeleaf 是一个跟 Velocity、FreeMarker 类似的模板引擎,它可以完全替代 JSP 。相较与其他的模板引擎,它有如下三个极吸引人的特点:动静结合:Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。开箱即用:它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、该jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。多方言支持:Thymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。与SpringBoot完美整合,SpringBoot提供了Thymeleaf的默认配置,并且为Thymeleaf设置了视图解析器,我们可以像以前操作jsp一样来操作Thymeleaf。代码几乎没有任何区别,就是在模板语法上有区别。接下来,我们就通过入门案例来体会Thymeleaf的魅力:6.2.编写接口编写一个controller,返回一些用户数据,放入模型中,等会在页面渲染@GetMapping("/all") public String all(ModelMap model) { // 查询用户 List<User> users = this.userService.queryAll(); // 放入模型 model.addAttribute("users", users); // 返回模板名称(就是classpath:/templates/目录下的html文件名) return "users"; }6.3.引入启动器直接引入启动器:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>SpringBoot会自动为Thymeleaf注册一个视图解析器:与解析JSP的InternalViewResolver类似,Thymeleaf也会根据前缀和后缀来确定模板文件的位置:默认前缀:classpath:/templates/默认后缀:.html所以如果我们返回视图:users,会指向到 classpath:/templates/users.html一般我们无需进行修改,默认即可。6.4.静态页面根据上面的文档介绍,模板默认放在classpath下的templates文件夹,我们新建一个html文件放入其中:编写html模板,渲染模型中的数据:注意,把html 的名称空间,改成:xmlns:th="http://www.thymeleaf.org" 会有语法提示<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>首页</title> <style type="text/css"> table {border-collapse: collapse; font-size: 14px; width: 80%; margin: auto} table, th, td {border: 1px solid darkslategray;padding: 10px} </style> </head> <body> <div style="text-align: center"> <span style="color: darkslategray; font-size: 30px">欢迎光临!</span> <hr/> <table class="list"> <tr> <th>id</th> <th>姓名</th> <th>用户名</th> <th>年龄</th> <th>性别</th> <th>生日</th> <th>备注</th> </tr> <tr th:each="user : ${users}"> <td th:text="${user.id}">1</td> <td th:text="${user.name}">张三</td> <td th:text="${user.userName}">zhangsan</td> <td th:text="${user.age}">20</td> <td th:text="${user.sex} == 1 ? '男': '女'">男</td> <td th:text="${#dates.format(user.birthday, 'yyyy-MM-dd')}">1980-02-30</td> <td th:text="${user.note}">1</td> </tr> </table> </div> </body> </html>我们看到这里使用了以下语法:${} :这个类似与el表达式,但其实是ognl的语法,比el表达式更加强大th-指令:th-是利用了Html5中的自定义属性来实现的。如果不支持H5,可以用data-th-来代替th:each:类似于c:foreach 遍历集合,但是语法更加简洁th:text:声明标签中的文本例如<td th-text='${user.id}'>1</td>,如果user.id有值,会覆盖默认的1如果没有值,则会显示td中默认的1。这正是thymeleaf能够动静结合的原因,模板解析失败不影响页面的显示效果,因为会显示默认值!6.5.测试接下来,我们打开页面测试一下:6.6.模板缓存Thymeleaf会在第一次对模板解析之后进行缓存,极大的提高了并发处理能力。但是这给我们开发带来了不便,修改页面后并不会立刻看到效果,我们开发阶段可以关掉缓存使用:# 开发阶段关闭thymeleaf的模板缓存 spring.thymeleaf.cache=false注意: 在Idea中,我们需要在修改页面后按快捷键:Ctrl + Shift + F9 对项目进行rebuild才可以。 eclipse中没有测试过。我们可以修改页面,测试一下。
2022年04月14日
60 阅读
0 评论
0 点赞
2022-04-13
扣减库存【方案设计】
今天我们来探讨下扣减库存的方案。生活中,我们总是用各种电商 APP 抢购商品,但是库存数是很少的,特别是秒杀场景,商品可能就一件,那如何保证不会出现超卖的情况呢?一、扣减库存的三种方案1.1 下单减库存 用户下单时减库存优点:实时减库存,避免付款时因库存不足减库存的问题缺点:恶意买家大量下单,将库存用完,但是不付款,真正想买的人买不到1.2 付款减库存 下单页面显示最新的库存,下单时不会立即减库存,而是等到支付时才会减库存。优点:防止恶意买家大量下单用光库存,避免下单减库存的缺点缺点:下单页面显示的库存数可能不是最新的库存数,而库存数用完后,下单页面的库存数没有刷新,出现下单数超过库存数,若支付的订单数超过库存数,则会出现支付失败。1.3 预扣库存 下单页面显示最新的库存,下单后保留这个库存一段时间(比如10分钟),超过保留时间后,库存释放。若保留时间过后再支付,如果没有库存,则支付失败。优点:结合下单减库存的优点,实时减库存,且缓解恶意买家大量下单的问题,保留时间内未支付,则释放库存。缺点:保留时间内,恶意买家大量下单将库存用完。并发量很高的时候,依然会出现下单数超过库存数。如何解决恶意买家下单的问题这里的恶意买家指短时间内大量下单,将库存用完的买家。2.1 限制用户下单数量优点:限制恶意买家下单缺点:用户想要多买几件,被限制了,会降低销售量2.2 标识恶意买家通过标识用户的设备 id 或者会员 id,将用户加入黑名单,不足之处是有些用户是模拟的,识别不出来是不是真正的恶意买家。如何解决下单成功而支付失败(库存不足)的问题3.1 备用库存商品库存用完后,如果还有用户支付,直接扣减备用库存。优点:缓解部分用户支付失败的问题。缺点:备用库存只能缓解问题,不能从根本上解决问题。另外备用库存针对普通商品可以,针对特殊商品这种库存少的,备用库存量也不会很大,还是会出现大量用户下单成功却因库存不足而支付失败的问题。如何解决高并发下库存超卖的场景库存超卖最简单的解释就是多成交了订单而发不了货。场景用户 A 和 B 成功下单,在支付时扣减库存,当前库存数为 10。因 A 和 B 查询库存时,都还有库存数,所以 A 和 B 都可以付款。A 和 B 同时支付,A 和 B 支付完成后,可以看做两个请求回调后台系统扣减库存,有两个线程处理请求,两个线程查询出来的库存数 inventory = 10。然后 A 线程更新最终库存数 : lastInventory = inventory - 1 = 9,B 线程更新库存数: lastInventory = inventory - 1 = 9。而实际最终的库存应是 8 才对,这样就出现库存超卖的情况,而发不出货。那如何解决库存超卖的情况呢?以下方案都是基于数据库层面的。有些同学可能会问,是不是可以用 Redis 分布式锁来,后面会讲到。 方案一 SQL语句直接更新库存,而不是先查询出来,然后赋值UPDATE [库存表] SET 库存数 - 1 方案二 SQL语句更新库存时,如果扣减库存后,库存数为负数,直接抛异常,利用事务的原子性进行自动回滚。方案三利用SQL语句更新库存,防止库存为负数UPDATE [库存表] SET 库存数 - 1 WHERE 库存数 - 1 > 0如果影响条数大于1,则表示扣减库存成功,否则不更新库存,并退款。秒杀场景下如何扣减库存5.1 采用下单减库存因秒杀场景下,大部分用户都是想直接购买商品的,可以直接用下单减库存。大量用户和恶意用户都是同时进行的,区别是正常用户会直接购买商品,恶意用户虽然在竞争抢购的名额,但是获取到的资格和普通用户一样,所以下单减库存在秒杀场景下,恶意用户下单并不能造成之前说的缺点。而且下单直接扣减库存,这个方案更简单,在第一步就扣减库存了。5.2 Redis 缓存查询缓存要比查询数据库快,所以将库存数放在缓存中,直接在缓存中扣减库存。如果并发很高,还可以采取分布式锁的方案。分布式锁可以参考我之前写的两篇文章:《Redis 分布式锁|从青铜到钻石的五种演进方案》《分布式锁中的王者方案 - Redisson》5.3 限流秒杀场景中,对请求做了很多限流操作,比如前端页面的限流和后端令牌桶限流,真正到扣减库存那一步时,请求数很少了。所以限流常用在秒杀方案中,感觉可以再写一篇限流的文章了~另外其实真实的项目中,用到了更多的机制来保证能够正常扣减库存,本篇是抛砖引入,希望大家提出宝贵的建议和方案~。赠送一张秒杀场景方案总结:
2022年04月13日
53 阅读
0 评论
0 点赞
2022-04-13
用设计模式优化代码
平时我们写代码呢,多数情况都是流水线式写代码,基本就可以实现业务逻辑了。如何在写代码中找到乐趣呢,我觉得,最好的方式就是:使用设计模式优化自己的业务代码。今天跟大家聊聊日常工作中,我都使用过哪些设计模式。策略模式1.1 业务场景 假设有这样的业务场景,大数据系统把文件推送过来,根据不同类型采取不同的解析方式。多数的小伙伴就会写出以下的代码。if(type=="A"){ //按照A格式解析 }else if(type=="B"){ //按B格式解析 }else{ //按照默认格式解析 }这个代码可能会存在哪些问题呢?如果分支变多,这里的代码就会变得臃肿,难以维护,可读性低。如果你需要接入一种新的解析类型,那只能在原有代码上修改。说得专业一点的话,就是以上代码,违背了面向对象编程的开闭原则以及单一原则。开闭原则 (对于扩展是开放的,但是对于修改是封闭的):增加或者删除某个逻辑,都需要修改到原来代码单一原则 (规定一个类应该只有一个发生变化的原因):修改任何类型的分支逻辑代码,都需要改动当前类的代码。如果你的代码就是酱紫:有多个if...else等条件分支,并且每个条件分支,可以封装起来替换的,我们就可以使用策略模式来优化。1.2 策略模式定义策略模式 定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的的客户。这个策略模式的定义是不是有点抽象呢?那我们来看点通俗易懂的比喻:假设你跟不同性格类型的小姐姐约会,要用不同的策略,有的请电影比较好,有的则去吃小吃效果不错,有的去逛街买买买最合适。当然,目的都是为了得到小姐姐的芳心,请看电影、吃小吃、逛街就是不同的策略。策略模式针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。1.3 策略模式使用策略模式怎么使用呢?酱紫实现的:一个接口或者抽象类,里面两个方法(一个方法匹配类型,一个可替换的逻辑实现方法)不同策略的差异化实现(就是说,不同策略的实现类)使用策略模式1.3.1 一个接口,两个方法public interface IFileStrategy { //属于哪种文件解析类型 FileTypeResolveEnum gainFileType(); //封装的公用算法(具体的解析方法) void resolve(Object objectparam); }1.3.2 不同策略的差异化实现A 类型策略具体实现@Component public class AFileResolve implements IFileStrategy { @Override public FileTypeResolveEnum gainFileType() { return FileTypeResolveEnum.File_A_RESOLVE; } @Override public void resolve(Object objectparam) { logger.info("A 类型解析文件,参数:{}",objectparam); //A类型解析具体逻辑 } }B 类型策略具体实现@Component public class BFileResolve implements IFileStrategy { @Override public FileTypeResolveEnum gainFileType() { return FileTypeResolveEnum.File_B_RESOLVE; } @Override public void resolve(Object objectparam) { logger.info("B 类型解析文件,参数:{}",objectparam); //B类型解析具体逻辑 } }默认类型策略具体实现@Component public class DefaultFileResolve implements IFileStrategy { @Override public FileTypeResolveEnum gainFileType() { return FileTypeResolveEnum.File_DEFAULT_RESOLVE; } @Override public void resolve(Object objectparam) { logger.info("默认类型解析文件,参数:{}",objectparam); //默认类型解析具体逻辑 } }1.3.3 使用策略模式 如何使用呢?我们借助spring的生命周期,使用ApplicationContextAware接口,把对应的策略,初始化到map里面。然后对外提供resolveFile方法即可。@Component public class StrategyUseService implements ApplicationContextAware{ private Map<FileTypeResolveEnum, IFileStrategy> iFileStrategyMap = new ConcurrentHashMap<>(); public void resolveFile(FileTypeResolveEnum fileTypeResolveEnum, Object objectParam) { IFileStrategy iFileStrategy = iFileStrategyMap.get(fileTypeResolveEnum); if (iFileStrategy != null) { iFileStrategy.resolve(objectParam); } } //把不同策略放到map @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { Map<String, IFileStrategy> tmepMap = applicationContext.getBeansOfType(IFileStrategy.class); tmepMap.values().forEach(strategyService -> iFileStrategyMap.put(strategyService.gainFileType(), strategyService)); } }责任链模式2.1 业务场景我们来看一个常见的业务场景,下订单。下订单接口,基本的逻辑,一般有参数非空校验、安全校验、黑名单校验、规则拦截等等。很多伙伴会使用异常来实现:public class Order { public void checkNullParam(Object param){ //参数非空校验 throw new RuntimeException(); } public void checkSecurity(){ //安全校验 throw new RuntimeException(); } public void checkBackList(){ //黑名单校验 throw new RuntimeException(); } public void checkRule(){ //规则拦截 throw new RuntimeException(); } public static void main(String[] args) { Order order= new Order(); try{ order.checkNullParam(); order.checkSecurity (); order.checkBackList(); order2.checkRule(); System.out.println("order success"); }catch (RuntimeException e){ System.out.println("order fail"); } } }这段代码使用了异常来做逻辑条件判断,如果后续逻辑越来越复杂的话,会出现一些问题:如异常只能返回异常信息,不能返回更多的字段,这时候需要自定义异常类。并且,阿里开发手册规定:禁止用异常做逻辑判断。【强制】 异常不要用来做流程控制,条件控制。说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。如何优化这段代码呢?可以考虑责任链模式。2.2 责任链模式定义当你想要让一个以上的对象有机会能够处理某个请求的时候,就使用责任链模式。责任链模式为请求创建了一个接收者对象的链。执行链上有多个对象节点,每个对象节点都有机会(条件匹配)处理请求事务,如果某个对象节点处理完了,就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。责任链模式实际上是一种处理请求的模式,它让多个处理器(对象节点)都有机会处理该请求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递:打个比喻:假设你晚上去上选修课,为了可以走点走,坐到了最后一排。来到教室,发现前面坐了好几个漂亮的小姐姐,于是你找张纸条,写上:“你好, 可以做我的女朋友吗?如果不愿意请向前传”。纸条就一个接一个的传上去了,后来传到第一排的那个妹子手上,她把纸条交给老师,听说老师40多岁未婚...2.3 责任链模式使用责任链模式怎么使用呢?一个接口或者抽象类每个对象差异化处理对象链(数组)初始化(连起来)2.3.1 一个接口或者抽象类这个接口或者抽象类,需要:有一个指向责任下一个对象的属性一个设置下一个对象的set方法给子类对象差异化实现的方法(如以下代码的doFilter方法)public abstract class AbstractHandler { //责任链中的下一个对象 private AbstractHandler nextHandler; /** * 责任链的下一个对象 */ public void setNextHandler(AbstractHandler nextHandler){ this.nextHandler = nextHandler; } /** * 具体参数拦截逻辑,给子类去实现 */ public void filter(Request request, Response response) { doFilter(request, response); if (getNextHandler() != null) { getNextHandler().filter(request, response); } } public AbstractHandler getNextHandler() { return nextHandler; } abstract void doFilter(Request filterRequest, Response response); }2.3.2 每个对象差异化处理责任链上,每个对象的差异化处理,如本小节的业务场景,就有参数校验对象、安全校验对象、黑名单校验对象、规则拦截对象/** * 参数校验对象 **/ @Component @Order(1) //顺序排第1,最先校验 public class CheckParamFilterObject extends AbstractHandler { @Override public void doFilter(Request request, Response response) { System.out.println("非空参数检查"); } } /** * 安全校验对象 */ @Component @Order(2) //校验顺序排第2 public class CheckSecurityFilterObject extends AbstractHandler { @Override public void doFilter(Request request, Response response) { //invoke Security check System.out.println("安全调用校验"); } } /** * 黑名单校验对象 */ @Component @Order(3) //校验顺序排第3 public class CheckBlackFilterObject extends AbstractHandler { @Override public void doFilter(Request request, Response response) { //invoke black list check System.out.println("校验黑名单"); } } /** * 规则拦截对象 */ @Component @Order(4) //校验顺序排第4 public class CheckRuleFilterObject extends AbstractHandler { @Override public void doFilter(Request request, Response response) { //check rule System.out.println("check rule"); } }2.3.3 对象链连起来(初始化)&& 使用@Component("ChainPatternDemo") public class ChainPatternDemo { //自动注入各个责任链的对象 @Autowired private List<AbstractHandler> abstractHandleList; private AbstractHandler abstractHandler; //spring注入后自动执行,责任链的对象连接起来 @PostConstruct public void initializeChainFilter(){ for(int i = 0;i<abstractHandleList.size();i++){ if(i == 0){ abstractHandler = abstractHandleList.get(0); }else{ AbstractHandler currentHander = abstractHandleList.get(i - 1); AbstractHandler nextHander = abstractHandleList.get(i); currentHander.setNextHandler(nextHander); } } } //直接调用这个方法使用 public Response exec(Request request, Response response) { abstractHandler.filter(request, response); return response; } public AbstractHandler getAbstractHandler() { return abstractHandler; } public void setAbstractHandler(AbstractHandler abstractHandler) { this.abstractHandler = abstractHandler; } }运行结果:非空参数检查 安全调用校验 校验黑名单 check rule模板方法模式3.1 业务场景假设我们有这么一个业务场景:内部系统不同商户,调用我们系统接口,去跟外部第三方系统交互(http方式)。走类似这么一个流程,如下:一个请求都会经历这几个流程:查询商户信息对请求报文加签发送http请求出去对返回的报文验签这里,有的商户可能是走代理出去的,有的是走直连。假设当前有A,B商户接入,不少伙伴可能这么实现,伪代码如下:// 商户A处理句柄 CompanyAHandler implements RequestHandler { Resp hander(req){ //查询商户信息 queryMerchantInfo(); //加签 signature(); //http请求(A商户假设走的是代理) httpRequestbyProxy() //验签 verify(); } } // 商户B处理句柄 CompanyBHandler implements RequestHandler { Resp hander(Rreq){ //查询商户信息 queryMerchantInfo(); //加签 signature(); // http请求(B商户不走代理,直连) httpRequestbyDirect(); // 验签 verify(); } }假设新加一个C商户接入,你需要再实现一套这样的代码。显然,这样代码就重复了,一些通用的方法,却在每一个子类都重新写了这一方法。如何优化呢?可以使用模板方法模式。3.2 模板方法模式定义定义一个操作中的算法的骨架流程,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。它的核心思想就是:定义一个操作的一系列步骤,对于某些暂时确定不下来的步骤,就留给子类去实现,这样不同的子类就可以定义出不同的步骤。打个通俗的比喻:模式举例:追女朋友要先“牵手”,再“拥抱”,再“接吻”, 再“拍拍..额..手”。至于具体你用左手还是右手牵,无所谓,但是整个过程,定了一个流程模板,按照模板来就行。3.3 模板方法使用一个抽象类,定义骨架流程(抽象方法放一起)确定的共同方法步骤,放到抽象类(去除抽象方法标记)不确定的步骤,给子类去差异化实现我们继续那以上的举例的业务流程例子,来一起用 模板方法优化一下哈:3.3.1 一个抽象类,定义骨架流程因为一个个请求经过的流程为一下步骤:查询商户信息对请求报文加签发送http请求出去对返回的报文验签所以我们就可以定义一个抽象类,包含请求流程的几个方法,方法首先都定义为抽象方法哈:/** * 抽象类定义骨架流程(查询商户信息,加签,http请求,验签) */ abstract class AbstractMerchantService { //查询商户信息 abstract queryMerchantInfo(); //加签 abstract signature(); //http 请求 abstract httpRequest(); // 验签 abstract verifySinature(); }3.3.2 确定的共同方法步骤,放到抽象类abstract class AbstractMerchantService { //模板方法流程 Resp handlerTempPlate(req){ //查询商户信息 queryMerchantInfo(); //加签 signature(); //http 请求 httpRequest(); // 验签 verifySinature(); } // Http是否走代理(提供给子类实现) abstract boolean isRequestByProxy(); }3.3.3 不确定的步骤,给子类去差异化实现因为是否走代理流程是不确定的,所以给子类去实现。商户A的请求实现:CompanyAServiceImpl extends AbstractMerchantService{ Resp hander(req){ return handlerTempPlate(req); } //走http代理的 boolean isRequestByProxy(){ return true; }商户B的请求实现:CompanyBServiceImpl extends AbstractMerchantService{ Resp hander(req){ return handlerTempPlate(req); } //公司B是不走代理的 boolean isRequestByProxy(){ return false; }观察者模式4.1 业务场景登陆注册应该是最常见的业务场景了。就拿注册来说事,我们经常会遇到类似的场景,就是用户注册成功后,我们给用户发一条消息,又或者发个邮件等等,因此经常有如下的代码:void register(User user){ insertRegisterUser(user); sendIMMessage(); sendEmail(); }这块代码会有什么问题呢?如果产品又加需求:现在注册成功的用户,再给用户发一条短信通知。于是你又得改register方法的代码了。。。这是不是违反了开闭原则啦。void register(User user){ insertRegisterUser(user); sendIMMessage(); sendMobileMessage(); sendEmail(); }并且,如果调发短信的接口失败了,是不是又影响到用户注册了?!这时候,是不是得加个异步方法给通知消息才好。实际上,我们可以使用观察者模式优化。4.2 观察者模式定义 观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被完成业务的更新。观察者模式属于行为模式,一个对象(被观察者)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。它的主要成员就是观察者和被观察者。被观察者(Observerable):目标对象,状态发生变化时,将通知所有的观察者。观察者(observer):接受被观察者的状态变化通知,执行预先定义的业务。使用场景: 完成某件事情后,异步通知场景。如,登陆成功,发个IM消息等等。4.3 观察者模式使用观察者模式实现的话,还是比较简单的。一个被观察者的类Observerable ;多个观察者Observer ;观察者的差异化实现经典观察者模式封装:EventBus实战4.3.1 一个被观察者的类Observerable 和 多个观察者Observerpublic class Observerable { private List<Observer> observers = new ArrayList<Observer>(); private int state; public int getState() { return state; } public void setState(int state) { notifyAllObservers(); } //添加观察者 public void addServer(Observer observer){ observers.add(observer); } //移除观察者 public void removeServer(Observer observer){ observers.remove(observer); } //通知 public void notifyAllObservers(int state){ if(state!=1){ System.out.println(“不是通知的状态”); return ; } for (Observer observer : observers) { observer.doEvent(); } } }4.3.2 观察者的差异化实现 //观察者 interface Observer { void doEvent(); } //Im消息 IMMessageObserver implements Observer{ void doEvent(){ System.out.println("发送IM消息"); } } //手机短信 MobileNoObserver implements Observer{ void doEvent(){ System.out.println("发送短信消息"); } } //EmailNo EmailObserver implements Observer{ void doEvent(){ System.out.println("发送email消息"); } }4.3.3 EventBus实战自己搞一套观察者模式的代码,还是有点小麻烦。实际上,Guava EventBus就封装好了,它 提供一套基于注解的事件总线,api可以灵活的使用,爽歪歪。我们来看下EventBus的实战代码哈,首先可以声明一个EventBusCenter类,它类似于以上被观察者那种角色Observerable。public class EventBusCenter { private static EventBus eventBus = new EventBus(); private EventBusCenter() { } public static EventBus getInstance() { return eventBus; } //添加观察者 public static void register(Object obj) { eventBus.register(obj); } //移除观察者 public static void unregister(Object obj) { eventBus.unregister(obj); } //把消息推给观察者 public static void post(Object obj) { eventBus.post(obj); } }然后再声明观察者EventListenerpublic class EventListener { @Subscribe //加了订阅,这里标记这个方法是事件处理方法 public void handle(NotifyEvent notifyEvent) { System.out.println("发送IM消息" + notifyEvent.getImNo()); System.out.println("发送短信消息" + notifyEvent.getMobileNo()); System.out.println("发送Email消息" + notifyEvent.getEmailNo()); } } //通知事件类 public class NotifyEvent { private String mobileNo; private String emailNo; private String imNo; public NotifyEvent(String mobileNo, String emailNo, String imNo) { this.mobileNo = mobileNo; this.emailNo = emailNo; this.imNo = imNo; } }使用demo测试:public class EventBusDemoTest { public static void main(String[] args) { EventListener eventListener = new EventListener(); EventBusCenter.register(eventListener); EventBusCenter.post(new NotifyEvent("13372817283", "123@qq.com", "666")); } }运行结果:发送IM消息666 发送短信消息13372817283 发送Email消息123@qq.com工厂模式5.1 业务场景工厂模式一般配合策略模式一起使用。用来去优化大量的if...else...或switch...case...条件语句。我们就取第一小节中策略模式那个例子吧。根据不同的文件解析类型,创建不同的解析对象 IFileStrategy getFileStrategy(FileTypeResolveEnum fileType){ IFileStrategy fileStrategy ; if(fileType=FileTypeResolveEnum.File_A_RESOLVE){ fileStrategy = new AFileResolve(); }else if(fileType=FileTypeResolveEnum.File_A_RESOLV){ fileStrategy = new BFileResolve(); }else{ fileStrategy = new DefaultFileResolve(); } return fileStrategy; }其实这就是工厂模式,定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。策略模式的例子,没有使用上一段代码,而是借助spring的特性,搞了一个工厂模式,哈哈,小伙伴们可以回去那个例子细品一下,我把代码再搬下来,小伙伴们再品一下吧:@Component public class StrategyUseService implements ApplicationContextAware{ private Map<FileTypeResolveEnum, IFileStrategy> iFileStrategyMap = new ConcurrentHashMap<>(); //把所有的文件类型解析的对象,放到map,需要使用时,信手拈来即可。这就是工厂模式的一种体现啦 @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { Map<String, IFileStrategy> tmepMap = applicationContext.getBeansOfType(IFileStrategy.class); tmepMap.values().forEach(strategyService -> iFileStrategyMap.put(strategyService.gainFileType(), strategyService)); } }5.2 使用工厂模式定义工厂模式也是比较简单的:一个工厂接口,提供一个创建不同对象的方法。其子类实现工厂接口,构造不同对象使用工厂模式5.3.1 一个工厂接口interface IFileResolveFactory{ void resolve(); }5.3.2 不同子类实现工厂接口class AFileResolve implements IFileResolveFactory{ void resolve(){ System.out.println("文件A类型解析"); } } class BFileResolve implements IFileResolveFactory{ void resolve(){ System.out.println("文件B类型解析"); } } class DefaultFileResolve implements IFileResolveFactory{ void resolve(){ System.out.println("默认文件类型解析"); } }5.3.3 使用工厂模式//构造不同的工厂对象 IFileResolveFactory fileResolveFactory; if(fileType=“A”){ fileResolveFactory = new AFileResolve(); }else if(fileType=“B”){ fileResolveFactory = new BFileResolve(); }else{ fileResolveFactory = new DefaultFileResolve(); } fileResolveFactory.resolve();一般情况下,对于工厂模式,你不会看到以上的代码。工厂模式会跟配合其他设计模式如策略模式一起出现的。单例模式6.1 业务场景单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。I/O与数据库的连接,一般就用单例模式实现de的。Windows里面的Task Manager(任务管理器)也是很典型的单例模式。来看一个单例模式的例子public class LanHanSingleton { private static LanHanSingleton instance; private LanHanSingleton(){ } public static LanHanSingleton getInstance(){ if (instance == null) { instance = new LanHanSingleton(); } return instance; } }以上的例子,就是懒汉式的单例实现。实例在需要用到的时候,才去创建,就比较懒。如果有则返回,没有则新建,需要加下 synchronized关键字,要不然可能存在线性安全问题。6.2 单例模式的经典写法其实单例模式还有有好几种实现方式,如饿汉模式,双重校验锁,静态内部类,枚举等实现方式。6.2.1 饿汉模式public class EHanSingleton { private static EHanSingleton instance = new EHanSingleton(); private EHanSingleton(){ } public static EHanSingleton getInstance() { return instance; } }饿汉模式,它比较饥饿、比较勤奋,实例在初始化的时候就已经建好了,不管你后面有没有用到,都先新建好实例再说。这个就没有线程安全的问题,但是呢,浪费内存空间呀。6.2.2 双重校验锁public class DoubleCheckSingleton { private volatile static DoubleCheckSingleton instance; private DoubleCheckSingleton() { } public static DoubleCheckSingleton getInstance(){ if (instance == null) { synchronized (DoubleCheckSingleton.class) { if (instance == null) { instance = new DoubleCheckSingleton(); } } } return instance; } }双重校验锁实现的单例模式,综合了懒汉式和饿汉式两者的优缺点。以上代码例子中,在synchronized关键字内外都加了一层 if条件判断,这样既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。6.2.3 静态内部类public class InnerClassSingleton { private static class InnerClassSingletonHolder{ private static final InnerClassSingleton INSTANCE = new InnerClassSingleton(); } private InnerClassSingleton(){} public static final InnerClassSingleton getInstance(){ return InnerClassSingletonHolder.INSTANCE; } }静态内部类的实现方式,效果有点类似双重校验锁。但这种方式只适用于静态域场景,双重校验锁方式可在实例域需要延迟初始化时使用。6.2.4 枚举public enum SingletonEnum { INSTANCE; public SingletonEnum getInstance(){ return INSTANCE; } }枚举实现的单例,代码简洁清晰。并且它还自动支持序列化机制,绝对防止多次实例化。
2022年04月13日
60 阅读
0 评论
0 点赞
2022-04-13
优雅的实现SpringBoot 如何实现异步编程
为什么要用异步框架,它解决什么问题?答:在SpringBoot的日常开发中,一般都是同步调用的。但实际中有很多场景非常适合使用异步来处理,如:注册新用户,送100个积分;或下单成功,发送push消息等等。就拿注册新用户这个用例来说,为什么要异步处理? 第一个原因:容错性、健壮性,如果送积分出现异常,不能因为送积分而导致用户注册失败;因为用户注册是主要功能,送积分是次要功能,即使送积分异常也要提示用户注册成功,然后后面在针对积分异常做补偿处理。 第二个原因:提升性能,例如注册用户花了20毫秒,送积分花费50毫秒,如果用同步的话,总耗时70毫秒,用异步的话,无需等待积分,故耗时20毫秒。 故,异步能解决2个问题,性能和容错性。SpringBoot如何实现异步调用?答:对于异步方法调用,从Spring3开始提供了@Async注解,我们只需要在方法上标注此注解,此方法即可实现异步调用。当然,我们还需要一个配置类,通过Enable模块驱动注解@EnableAsync 来开启异步功能。为什么不要使用默认的线程池?答:使用@Async注解,在默认情况下用的是SimpleAsyncTaskExecutor线程池,该线程池不是真正意义上的线程池。使用此线程池无法实现线程重用,每次调用都会新建一条线程。若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。所以我们在使用Spring中的@Async异步框架时一定要自定义线程池,替代默认的SimpleAsyncTaskExecutor。项目实战为@Async实现一个自定义线程池@Configuration @EnableAsync public class SyncConfiguration { @Bean(name = "asyncPoolTaskExecutor") public ThreadPoolTaskExecutor executor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); //核心线程数 taskExecutor.setCorePoolSize(10); //线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程 taskExecutor.setMaxPoolSize(100); //缓存队列 taskExecutor.setQueueCapacity(50); //许的空闲时间,当超过了核心线程出之外的线程在空闲时间到达之后会被销毁 taskExecutor.setKeepAliveSeconds(200); //异步方法内部线程名称 taskExecutor.setThreadNamePrefix("async-"); /** * 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略 * 通常有以下四种策略: * ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 * ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 * ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) * ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功 */ taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; } }配置自定义线程池以后我们就可以大胆的使用@Async提供的异步处理能力了。 多个线程池处理 在现实的互联网项目开发中,针对高并发的请求,一般的做法是高并发接口单独线程池隔离处理。假设现在2个高并发接口:一个是修改用户信息接口,刷新用户redis缓存;一个是下订单接口,发送app push信息。往往会根据接口特征定义两个线程池,这时候我们在使用@Async时就需要通过指定线程池名称进行区分。 为@Async指定线程池名字@SneakyThrows @Async("asyncPoolTaskExecutor") public void doTask1() { long t1 = System.currentTimeMillis(); Thread.sleep(2000); long t2 = System.currentTimeMillis(); log.info("task1 cost {} ms" , t2-t1); } 当系统存在多个线程池时,我们也可以配置一个默认线程池,对于非默认的异步任务再通过@Async("otherTaskExecutor")来指定线程池名称。 配置默认线程池 可以修改配置类让其实现AsyncConfigurer,并重写getAsyncExecutor()方法,指定默认线程池:@Configuration @EnableAsync @Slf4j public class AsyncConfiguration implements AsyncConfigurer { @Bean(name = "asyncPoolTaskExecutor") public ThreadPoolTaskExecutor executor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); //核心线程数 taskExecutor.setCorePoolSize(2); //线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程 taskExecutor.setMaxPoolSize(10); //缓存队列 taskExecutor.setQueueCapacity(50); //许的空闲时间,当超过了核心线程出之外的线程在空闲时间到达之后会被销毁 taskExecutor.setKeepAliveSeconds(200); //异步方法内部线程名称 taskExecutor.setThreadNamePrefix("async-"); /** * 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略 * 通常有以下四种策略: * ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 * ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 * ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) * ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功 */ taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; } /** * 指定默认线程池 */ @Override public Executor getAsyncExecutor() { return executor(); } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (ex, method, params) -> log.error("线程池执行任务发送未知错误,执行方法:{}",method.getName(),ex); } }如下,doTask1()方法使用默认使用线程池asyncPoolTaskExecutor,doTask2()使用线程池otherTaskExecutor,非常灵活。@Async public void doTask1() { long t1 = System.currentTimeMillis(); Thread.sleep(2000); long t2 = System.currentTimeMillis(); log.info("task1 cost {} ms" , t2-t1); } @SneakyThrows @Async("otherTaskExecutor") public void doTask2() { long t1 = System.currentTimeMillis(); Thread.sleep(3000); long t2 = System.currentTimeMillis(); log.info("task2 cost {} ms" , t2-t1); }
2022年04月13日
42 阅读
0 评论
0 点赞
2022-04-13
SpringBoot 实现各种参数校验
书接上回,在上个回合我们介绍了如何优雅的实现业务参数的校验,通过自定义注解的方式我们实现了对用户数据的校验,今天我们再来分享一下SpringBoot 实现各种参数校验。简单使用引入依赖requestBody参数校验requestParam/PathVariable参数校验统一异常处理进阶使用分组校验嵌套校验集合校验自定义校验编程式校验快速失败(Fail Fast)@Valid和@Validated区别实现原理@Valid和@Validated区别方法级别的参数校验实现原理{mtitle title="简单使用"/} Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。 Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。接下来,我们以spring-boot项目为例,介绍Spring Validation的使用。引入依赖<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.1.Final</version> </dependency>对于web服务来说,为防止非法参数对业务造成影响,在Controller层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:POST、PUT请求,使用requestBody传递参数;GET请求,使用requestParam/PathVariable传递参数。下面我们简单介绍下requestBody和requestParam/PathVariable的参数校验实战requestBody参数校验 POST、PUT请求一般会使用requestBody传递参数,这种情况下,后端使用DTO对象进行接收。只要给DTO对象加上@Validated注解就能实现自动参数校验。比如,有一个保存User的接口,要求userName长度是2-10,account和password字段长度是6-20。 如果校验失败,会抛出MethodArgumentNotValidException异常,Spring默认会将其转为400(Bad Request)请求。 在DTO字段上声明约束注解@Data public class UserDTO { private Long userId; @NotNull @Length(min = 2, max = 10) private String userName; @NotNull @Length(min = 6, max = 20) private String account; @NotNull @Length(min = 6, max = 20) private String password; }在方法参数上声明校验注解@PostMapping("/save") public Result saveUser(@RequestBody @Validated UserDTO userDTO) { // 校验通过,才会执行业务逻辑处理 return Result.ok(); }requestParam/PathVariable参数校验 GET请求一般会使用requestParam/PathVariable传参。如果参数比较多(比如超过6个),还是推荐使用DTO对象接收。否则,推荐将一个个参数平铺到方法入参中。在这种情况下,必须在Controller类上标注@Validated注解,并在入参上声明约束注解(如@Min等)。如果校验失败,会抛出ConstraintViolationException异常。@RequestMapping("/api/user") @RestController @Validated public class UserController { // 路径变量 @GetMapping("{userId}") public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) { // 校验通过,才会执行业务逻辑处理 UserDTO userDTO = new UserDTO(); userDTO.setUserId(userId); userDTO.setAccount("11111111111111111"); userDTO.setUserName("xixi"); userDTO.setAccount("11111111111111111"); return Result.ok(userDTO); } // 查询参数 @GetMapping("getByAccount") public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) { // 校验通过,才会执行业务逻辑处理 UserDTO userDTO = new UserDTO(); userDTO.setUserId(10000000000000003L); userDTO.setAccount(account); userDTO.setUserName("xixi"); userDTO.setAccount("11111111111111111"); return Result.ok(userDTO); } }统一异常处理 前面说过,如果校验失败,会抛出MethodArgumentNotValidException或者ConstraintViolationException异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。比如我们系统要求无论发送什么异常,http的状态码必须返回200,由业务码去区分系统的异常情况。@RestControllerAdvice public class CommonExceptionHandler { @ExceptionHandler({MethodArgumentNotValidException.class}) @ResponseStatus(HttpStatus.OK) @ResponseBody public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { BindingResult bindingResult = ex.getBindingResult(); StringBuilder sb = new StringBuilder("校验失败:"); for (FieldError fieldError : bindingResult.getFieldErrors()) { sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", "); } String msg = sb.toString(); return Result.fail(BusinessCode.参数校验失败, msg); } @ExceptionHandler({ConstraintViolationException.class}) @ResponseStatus(HttpStatus.OK) @ResponseBody public Result handleConstraintViolationException(ConstraintViolationException ex) { return Result.fail(BusinessCode.参数校验失败, ex.getMessage()); } }{mtitle title="进阶使用"/}分组校验 在实际项目中,可能多个方法需要使用同一个DTO类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在DTO类的字段上加约束注解无法解决这个问题。因此,spring-validation支持了分组校验的功能,专门用来解决这类问题。还是上面的例子,比如保存User的时候,UserId是可空的,但是更新User的时候,UserId的值必须>=10000000000000000L;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下: 约束注解上声明适用的分组信息groups@Data public class UserDTO { @Min(value = 10000000000000000L, groups = Update.class) private Long userId; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String userName; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String account; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String password; /** * 保存的时候校验分组 */ public interface Save { } /** * 更新的时候校验分组 */ public interface Update { } }@Validated注解上指定校验分组@PostMapping("/save") public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) { // 校验通过,才会执行业务逻辑处理 return Result.ok(); } @PostMapping("/update") public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) { // 校验通过,才会执行业务逻辑处理 return Result.ok(); }嵌套校验 前面的示例中,DTO类里面的字段都是基本数据类型和String类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。比如,上面保存User信息的时候同时还带有Job信息。需要注意的是,此时DTO类的对应字段必须标记@Valid注解。@Data public class UserDTO { @Min(value = 10000000000000000L, groups = Update.class) private Long userId; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String userName; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String account; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String password; @NotNull(groups = {Save.class, Update.class}) @Valid private Job job; @Data public static class Job { @Min(value = 1, groups = Update.class) private Long jobId; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String jobName; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String position; } /** * 保存的时候校验分组 */ public interface Save { } /** * 更新的时候校验分组 */ public interface Update { } } 嵌套校验可以结合分组校验一起使用。还有就是嵌套集合校验会对集合里面的每一项都进行校验,例如List字段会对这个list里面的每一个Job对象都进行校验.集合校验 如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数:包装List类型,并声明@Valid注解public class ValidationList<E> implements List<E> { @Delegate // @Delegate是lombok注解 @Valid // 一定要加@Valid注解 public List<E> list = new ArrayList<>(); // 一定要记得重写toString方法 @Override public String toString() { return list.toString(); } } @Delegate注解受lombok版本限制,1.18.6以上版本可支持。如果校验不通过,会抛出NotReadablePropertyException,同样可以使用统一异常进行处理。 比如,我们需要一次性保存多个User对象,Controller层的方法可以这么写:@PostMapping("/saveList") public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) { // 校验通过,才会执行业务逻辑处理 return Result.ok(); }自定义校验 业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。自定义spring validation非常简单,假设我们自定义加密id(由数字或者a-f的字母组成,32-256长度)校验,主要分为两步: 自定义约束注解:@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {EncryptIdValidator.class}) public @interface EncryptId { // 默认错误消息 String message() default "加密id格式错误"; // 分组 Class<?>[] groups() default {}; // 负载 Class<? extends Payload>[] payload() default {}; }实现ConstraintValidator接口编写约束校验器public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> { private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 不为null才进行校验 if (value != null) { Matcher matcher = PATTERN.matcher(value); return matcher.find(); } return true; } }这样我们就可以使用@EncryptId进行参数校验了!编程式校验上面的示例都是基于注解来实现自动校验的,在某些情况下,我们可能希望以编程方式调用验证。这个时候可以注入javax.validation.Validator对象,然后再调用其api。@Autowired private javax.validation.Validator globalValidator; // 编程式校验 @PostMapping("/saveWithCodingValidate") public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) { Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class); // 如果校验通过,validate为空;否则,validate包含未校验通过项 if (validate.isEmpty()) { // 校验通过,才会执行业务逻辑处理 } else { for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) { // 校验失败,做其它逻辑 System.out.println(userDTOConstraintViolation); } } return Result.ok(); }快速失败(Fail Fast)Spring Validation默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启Fali Fast模式,一旦校验失败就立即返回。@Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() // 快速失败模式 .failFast(true) .buildValidatorFactory(); return validatorFactory.getValidator(); }8.@Valid和@Validated区别 {mtitle title="实现原理"/}requestBody参数校验实现原理在spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody标注的参数以及处理@ResponseBody标注方法的返回值的。显然,执行参数校验的逻辑肯定就在解析参数的方法resolveArgument()中:public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter.nestedIfOptional(); //将请求数据封装到DTO对象中 Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); String name = Conventions.getVariableNameForParameter(parameter); if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) { // 执行数据校验 validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } } return adaptArgumentIfNecessary(arg, parameter); } }可以看到,resolveArgument()调用了validateIfApplicable()进行参数校验。protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { // 获取参数注解,比如@RequestBody、@Valid、@Validated Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { // 先尝试获取@Validated注解 Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); //如果直接标注了@Validated,那么直接开启校验。 //如果没有,那么判断参数前是否有Valid起头的注解。 if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); //执行校验 binder.validate(validationHints); break; } } }看到这里,大家应该能明白为什么这种场景下@Validated、@Valid两个注解可以混用。我们接下来继续看WebDataBinder.validate()实现。@Override public void validate(Object target, Errors errors, Object... validationHints) { if (this.targetValidator != null) { processConstraintViolations( //此处调用Hibernate Validator执行真正的校验 this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); } }最终发现底层最终还是调用了Hibernate Validator进行真正的校验处理。方法级别的参数校验实现原理上面提到的将参数一个个平铺到方法参数中,然后在每个参数前面声明约束注解的校验方式,就是方法级别的参数校验。实际上,这种方式可用于任何Spring Bean的方法上,比如Controller/Service等。其底层实现原理就是AOP,具体来说是通过MethodValidationPostProcessor动态注册AOP切面,然后使用MethodValidationInterceptor对切点方法织入增强。public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean { @Override public void afterPropertiesSet() { //为所有`@Validated`标注的Bean创建切面 Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); //创建Advisor进行增强 this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); } //创建Advice,本质就是一个方法拦截器 protected Advice createMethodValidationAdvice(@Nullable Validator validator) { return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); } }接着看一下MethodValidationInterceptor:public class MethodValidationInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { //无需增强的方法,直接跳过 if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); } //获取分组信息 Class<?>[] groups = determineValidationGroups(invocation); ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set<ConstraintViolation<Object>> result; try { //方法入参校验,最终还是委托给Hibernate Validator来校验 result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { ... } //有异常直接抛出 if (!result.isEmpty()) { throw new ConstraintViolationException(result); } //真正的方法调用 Object returnValue = invocation.proceed(); //对返回值做校验,最终还是委托给Hibernate Validator来校验 result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); //有异常直接抛出 if (!result.isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; } }实际上,不管是requestBody参数校验还是方法级别的校验,最终都是调用Hibernate Validator执行校验,Spring Validation只是做了一层封装。
2022年04月13日
78 阅读
0 评论
0 点赞
2022-04-13
SpringBoot中实现业务参数校验
在日常的接口开发中,为了保证接口的稳定安全,我们一般需要在接口逻辑中处理两种校验:参数校验业务规则校验{mtitle title="参数校验"/} 参数校验很好理解,比如登录的时候需要校验用户名密码是否为空,创建用户的时候需要校验邮件、手机号码格式是否准确。而实现参数校验也非常简单,我们只需要使用Bean Validation校验框架即可,借助它提供的校验注解我们可以非常方便的完成参数校验。常见的校验注解有:@Null、 @NotNull、 @AssertTrue、 @AssertFalse、 @Min、 @Max、 @DecimalMin、 @DecimalMax、 @Negative、 @NegativeOrZero、 @Positive、 @PositiveOrZero、 @Size、 @Digits、 @Past、 @PastOrPresent、 @Future、 @FutureOrPresent、 @Pattern、 @NotEmpty、 @NotBlank、 @Email` {mtitle title="业务规则校验"/} 业务规则校验指接口需要满足某些特定的业务规则,举个例子:业务系统的用户需要保证其唯一性,用户属性不能与其他用户产生冲突,不允许与数据库中任何已有用户的用户名称、手机号码、邮箱产生重复。这就要求在创建用户时需要校验用户名称、手机号码、邮箱是否被注册;编辑用户时不能将信息修改成已有用户的属性。95%的程序员当面对这种业务规则校验时往往选择写在service逻辑中,常见的代码逻辑如下: public void create(User user) { Account account = accountDao.queryByUserNameOrPhoneOrEmail(user.getName(),user.getPhone(),user.getEmail()); if (account != null) { throw new IllegalArgumentException("用户已存在,请重新输入"); } } 虽然使用Assert来优化代码可以使其看上去更简洁,但是将简单的校验交给 Bean Validation,而把复杂的校验留给自己,这简直是买椟还珠故事的程序员版本。最优雅的实现方法应该是参考 Bean Validation 的标准方式,借助自定义校验注解完成业务规则校验。{mtitle title="代码实战"/} 需求很容易理解,注册新用户时,应约束不与任何已有用户的关键信息重复;而修改自己的信息时,只能与自己的信息重复,不允许修改成已有用户的信息。 这些约束规则不仅仅为这两个方法服务,它们可能会在用户资源中的其他入口被使用到,乃至在其他分层的代码中被使用到,在 Bean 上做校验就能全部覆盖上述这些使用场景。自定义注解首先我们需要创建两个自定义注解,用于业务规则校验:UniqueUser:表示一个用户是唯一的,唯一性包含:用户名,手机号码、邮箱@Documented @Retention(RUNTIME) @Target({FIELD, METHOD, PARAMETER, TYPE}) @Constraint(validatedBy = UserValidation.UniqueUserValidator.class) public @interface UniqueUser { String message() default "用户名、手机号码、邮箱不允许与现存用户重复"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }NotConflictUser:表示一个用户的信息是无冲突的,无冲突是指该用户的敏感信息与其他用户不重合@Documented @Retention(RUNTIME) @Target({FIELD, METHOD, PARAMETER, TYPE}) @Constraint(validatedBy = UserValidation.NotConflictUserValidator.class) public @interface NotConflictUser { String message() default "用户名称、邮箱、手机号码与现存用户产生重复"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }实现业务校验规则 想让自定义验证注解生效,需要实现 ConstraintValidator 接口。接口的第一个参数是 自定义注解类型 ,第二个参数是 被注解字段的类 ,因为需要校验多个参数,我们直接传入用户对象。需要提到的一点是 ConstraintValidator 接口的实现类无需添加 @Component 它在启动的时候就已经被加载到容器中了。@Slf4j public class UserValidation<T extends Annotation> implements ConstraintValidator<T, User> { protected Predicate<User> predicate = c -> true; @Resource protected UserRepository userRepository; @Override public boolean isValid(User user, ConstraintValidatorContext constraintValidatorContext) { return userRepository == null || predicate.test(user); } /** * 校验用户是否唯一 * 即判断数据库是否存在当前新用户的信息,如用户名,手机,邮箱 */ public static class UniqueUserValidator extends UserValidation<UniqueUser>{ @Override public void initialize(UniqueUser uniqueUser) { predicate = c -> !userRepository.existsByUserNameOrEmailOrTelphone(c.getUserName(),c.getEmail(),c.getTelphone()); } } /** * 校验是否与其他用户冲突 * 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突 */ public static class NotConflictUserValidator extends UserValidation<NotConflictUser>{ @Override public void initialize(NotConflictUser notConflictUser) { predicate = c -> { log.info("user detail is {}",c); Collection<User> collection = userRepository.findByUserNameOrEmailOrTelphone(c.getUserName(), c.getEmail(), c.getTelphone()); // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突 return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId())); }; } } }这里使用Predicate函数式接口对业务规则进行判断。使用示例@RestController @RequestMapping("/senior/user") @Slf4j @Validated public class UserController { @Autowired private UserRepository userRepository; @PostMapping public User createUser(@UniqueUser @Valid User user){ User savedUser = userRepository.save(user); log.info("save user id is {}",savedUser.getId()); return savedUser; } @SneakyThrows @PutMapping public User updateUser(@NotConflictUser @Valid @RequestBody User user){ User editUser = userRepository.save(user); log.info("update user is {}",editUser); return editUser; } }使用很简单,只需要在方法上加入自定义注解即可,业务逻辑中不需要添加任何业务规则的代码。错误示例{ "status": 400, "message": "用户名、手机号码、邮箱不允许与现存用户重复", "data": null, "timestamp": 1644309081037 }{mtitle title="小结"/} 通过上面几步操作,业务校验便和业务逻辑就完全分离开来,在需要校验时用@Validated注解自动触发,或者通过代码手动触发执行,可根据你们项目的要求,将这些注解应用于控制器、服务层、持久层等任何层次的代码之中。 这种方式比任何业务规则校验的方法都优雅,推荐大家在项目中使用。在开发时可以将不带业务含义的格式校验注解放到 Bean 的类定义之上,将带业务逻辑的校验放到 Bean 的类定义的外面。这两者的区别是放在类定义中的注解能够自动运行,而放到类外面则需要像前面代码那样,明确标出注解时才会运行。
2022年04月13日
66 阅读
0 评论
0 点赞
2022-04-12
基于AOP多数据源的实现
其实现在的项目或者中间件中,多数据源已经封装的很好了。比如mybatis-plus就封装好了多数据源,@DS("*")就可以实现对数据源的注入,具体可以参考: Mybatis-Plus多数据源,但是在历史的项目中确实也有用到多数据源,分享给大家,也作为自己职业生涯的经验记录。1.自定义DataSource注解/** * 多数据源注解 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface DataSource { String value() default ""; }2.定义注解的切面DataSourceAspectpackage com.bdysoft.datasource.aspect; import com.bdysoft.datasource.annotation.DataSource; import com.bdysoft.datasource.config.DynamicContextHolder; import com.bdysoft.datasource.annotation.DataSource; import com.bdysoft.datasource.config.DynamicContextHolder; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * 多数据源,切面处理类 */ @Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class DataSourceAspect { protected Logger logger = LoggerFactory.getLogger(getClass()); @Pointcut("@annotation(com.bdysoft.datasource.annotation.DataSource) " + "|| @within(com.bdysoft.datasource.annotation.DataSource)") public void dataSourcePointCut() { } @Around("dataSourcePointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Class targetClass = point.getTarget().getClass(); Method method = signature.getMethod(); DataSource targetDataSource = (DataSource)targetClass.getAnnotation(DataSource.class); DataSource methodDataSource = method.getAnnotation(DataSource.class); if(targetDataSource != null || methodDataSource != null){ String value; if(methodDataSource != null){ value = methodDataSource.value(); }else { value = targetDataSource.value(); } DynamicContextHolder.push(value); logger.debug("set datasource is {}", value); } try { return point.proceed(); } finally { DynamicContextHolder.poll(); logger.debug("clean datasource"); } } } 3.自定义多数据源上下文DynamicContextHolderpackage com.bdysoft.datasource.config; import java.util.ArrayDeque; import java.util.Deque; /** * 多数据源上下文 */ public class DynamicContextHolder { @SuppressWarnings("unchecked") private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = new ThreadLocal() { @Override protected Object initialValue() { return new ArrayDeque(); } }; /** * 获得当前线程数据源 * * @return 数据源名称 */ public static String peek() { return CONTEXT_HOLDER.get().peek(); } /** * 设置当前线程数据源 * * @param dataSource 数据源名称 */ public static void push(String dataSource) { CONTEXT_HOLDER.get().push(dataSource); } /** * 清空当前线程数据源 */ public static void poll() { Deque<String> deque = CONTEXT_HOLDER.get(); deque.poll(); if (deque.isEmpty()) { CONTEXT_HOLDER.remove(); } } } 4.多数据源DynamicDataSource继承AbstractRoutingDataSourcepackage com.bdysoft.datasource.config; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 多数据源 */ public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicContextHolder.peek(); } }5.配置多数据源DynamicDataSourceConfigpackage com.bdysoft.datasource.config; import com.alibaba.druid.pool.DruidDataSource; import com.bdysoft.datasource.properties.DataSourceProperties; import com.bdysoft.datasource.properties.DynamicDataSourceProperties; import com.bdysoft.datasource.properties.DataSourceProperties; import com.bdysoft.datasource.properties.DynamicDataSourceProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /** * 配置多数据源 */ @Configuration @EnableConfigurationProperties(DynamicDataSourceProperties.class) public class DynamicDataSourceConfig { @Autowired private DynamicDataSourceProperties properties; @Bean @ConfigurationProperties(prefix = "spring.datasource.druid") public DataSourceProperties dataSourceProperties() { return new DataSourceProperties(); } @Bean public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) { DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setTargetDataSources(getDynamicDataSource()); //默认数据源 DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties); dynamicDataSource.setDefaultTargetDataSource(defaultDataSource); return dynamicDataSource; } private Map<Object, Object> getDynamicDataSource(){ Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource(); Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size()); dataSourcePropertiesMap.forEach((k, v) -> { DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v); targetDataSources.put(k, druidDataSource); }); return targetDataSources; } } 6.编写DynamicDataSourceFactorypackage com.bdysoft.datasource.config; import com.alibaba.druid.pool.DruidDataSource; import com.bdysoft.datasource.properties.DataSourceProperties; import com.bdysoft.datasource.properties.DataSourceProperties; import java.sql.SQLException; /** * DruidDataSource */ public class DynamicDataSourceFactory { public static DruidDataSource buildDruidDataSource(DataSourceProperties properties) { DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setDriverClassName(properties.getDriverClassName()); druidDataSource.setUrl(properties.getUrl()); druidDataSource.setUsername(properties.getUsername()); druidDataSource.setPassword(properties.getPassword()); druidDataSource.setInitialSize(properties.getInitialSize()); druidDataSource.setMaxActive(properties.getMaxActive()); druidDataSource.setMinIdle(properties.getMinIdle()); druidDataSource.setMaxWait(properties.getMaxWait()); druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis()); druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis()); druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis()); druidDataSource.setValidationQuery(properties.getValidationQuery()); druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout()); druidDataSource.setTestOnBorrow(properties.isTestOnBorrow()); druidDataSource.setTestOnReturn(properties.isTestOnReturn()); druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements()); druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements()); druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements()); try { druidDataSource.setFilters(properties.getFilters()); druidDataSource.init(); } catch (SQLException e) { e.printStackTrace(); } return druidDataSource; } } 7.编写DataSourcePropertiespackage com.bdysoft.datasource.properties; /** * 多数据源属性 */ public class DataSourceProperties { private String driverClassName; private String url; private String username; private String password; /** * Druid默认参数 */ private int initialSize = 2; private int maxActive = 10; private int minIdle = -1; private long maxWait = 60 * 1000L; private long timeBetweenEvictionRunsMillis = 60 * 1000L; private long minEvictableIdleTimeMillis = 1000L * 60L * 30L; private long maxEvictableIdleTimeMillis = 1000L * 60L * 60L * 7; private String validationQuery = "select 1"; private int validationQueryTimeout = -1; private boolean testOnBorrow = false; private boolean testOnReturn = false; private boolean testWhileIdle = true; private boolean poolPreparedStatements = false; private int maxOpenPreparedStatements = -1; private boolean sharePreparedStatements = false; private String filters = "stat,wall"; public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getInitialSize() { return initialSize; } public void setInitialSize(int initialSize) { this.initialSize = initialSize; } public int getMaxActive() { return maxActive; } public void setMaxActive(int maxActive) { this.maxActive = maxActive; } public int getMinIdle() { return minIdle; } public void setMinIdle(int minIdle) { this.minIdle = minIdle; } public long getMaxWait() { return maxWait; } public void setMaxWait(long maxWait) { this.maxWait = maxWait; } public long getTimeBetweenEvictionRunsMillis() { return timeBetweenEvictionRunsMillis; } public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) { this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; } public long getMinEvictableIdleTimeMillis() { return minEvictableIdleTimeMillis; } public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) { this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis; } public long getMaxEvictableIdleTimeMillis() { return maxEvictableIdleTimeMillis; } public void setMaxEvictableIdleTimeMillis(long maxEvictableIdleTimeMillis) { this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis; } public String getValidationQuery() { return validationQuery; } public void setValidationQuery(String validationQuery) { this.validationQuery = validationQuery; } public int getValidationQueryTimeout() { return validationQueryTimeout; } public void setValidationQueryTimeout(int validationQueryTimeout) { this.validationQueryTimeout = validationQueryTimeout; } public boolean isTestOnBorrow() { return testOnBorrow; } public void setTestOnBorrow(boolean testOnBorrow) { this.testOnBorrow = testOnBorrow; } public boolean isTestOnReturn() { return testOnReturn; } public void setTestOnReturn(boolean testOnReturn) { this.testOnReturn = testOnReturn; } public boolean isTestWhileIdle() { return testWhileIdle; } public void setTestWhileIdle(boolean testWhileIdle) { this.testWhileIdle = testWhileIdle; } public boolean isPoolPreparedStatements() { return poolPreparedStatements; } public void setPoolPreparedStatements(boolean poolPreparedStatements) { this.poolPreparedStatements = poolPreparedStatements; } public int getMaxOpenPreparedStatements() { return maxOpenPreparedStatements; } public void setMaxOpenPreparedStatements(int maxOpenPreparedStatements) { this.maxOpenPreparedStatements = maxOpenPreparedStatements; } public boolean isSharePreparedStatements() { return sharePreparedStatements; } public void setSharePreparedStatements(boolean sharePreparedStatements) { this.sharePreparedStatements = sharePreparedStatements; } public String getFilters() { return filters; } public void setFilters(String filters) { this.filters = filters; } } 8.编写DynamicDataSourcePropertiespackage com.bdysoft.datasource.properties; import org.springframework.boot.context.properties.ConfigurationProperties; import java.util.LinkedHashMap; import java.util.Map; /** * 多数据源属性 */ @ConfigurationProperties(prefix = "dynamic") public class DynamicDataSourceProperties { private Map<String, DataSourceProperties> datasource = new LinkedHashMap<>(); public Map<String, DataSourceProperties> getDatasource() { return datasource; } public void setDatasource(Map<String, DataSourceProperties> datasource) { this.datasource = datasource; } }9.yam文件配置多数据源##多数据源的配置 dynamic: datasource: local: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/admin?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root user: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root10.代码中使用多数据源@DataSource("local")
2022年04月12日
56 阅读
0 评论
0 点赞
2022-04-09
接口签名鉴权
一:业务背景 接口开发是各系统之间对接的重要方式,其数据是通过开放的互联网传输,对数据的安全性要有一定要求。为了提高传输过程参数的防篡改性,签名sign的方式是目前比较常用的方式。二:实施方案鉴权方案1.公共鉴权参数(所有接口必须带有的参数,参数支持存放在HTTP headers【优先级高】或者url链接参数上)2.签名生成规则secret= 123456789(秘钥,内部协定)①所有业务接口服务 请求方式均为 POST, 请求参数类型Content-Type=application/json②sign 签名字符生成规则为 MD5( secret+client + format +time + version+ RequstBody(请求参数对象).toJSONString() + secret).toLowerCase()3.签名流程客户端: 按上述要求生成签名sign,并连同上述参数存入请求headers服务端: 验证流程(timeout默认时长15分钟)4.技术方案:后端使用过滤器进行统一接口鉴权,aksk+请求有效时长+有效时长内请求id不能重复,这样能鉴权+防篡改+防重放@Slf4j public class ApiSignAuthFilter implements Filter { /** * 秘钥 */ @Value private String secret; /** * 签名参数名称集合 */ private static String[] params = new String[]{"client", "cuid","format", "time", "version", "sign"}; private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher(); @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; //特殊场景,可以跳过验证(具体看业务) if (!StrUtil.equalsIgnoreCase(request.getMethod(), HttpMethod.POST.name()) || !StrUtil.containsIgnoreCase(request.getHeader(HttpHeaders.CONTENT_TYPE),MediaType.APPLICATION_JSON_VALUE)) { chain.doFilter(req, res); }else { //api参数签名校验 try { chain.doFilter(verifySign(request), res); }catch (BusinessException e) { HttpServletResponse response = (HttpServletResponse) res; response.setContentType("application/json;charset=UTF-8"); String originalURL = request.getHeader("Origin"); if (originalURL != null) { response.addHeader("Access-Control-Allow-Origin", originalURL); } response.addHeader("Access-Control-Allow-Credentials", "true"); PrintWriter out = response.getWriter(); out.append(JSONHelper.toJSONString(ResponseBean.error(e.getErrorCode(),e.getMessage()))); } } } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } /*** * 签名校验 * @param request */ private HttpServletRequestWrapper verifySign(HttpServletRequest request) { String client = getRequestParam(request, params[0]); // 客户端类型 if (StringUtil.isEmpty(cuid) && "wx".equals(client)) { cuid = getIpAddr(request); } String format = getRequestParam(request, params[2]); // 请求格式 json String time = getRequestParam(request, params[3]); // 请求时间 String version = getRequestParam(request, params[4]); // 应用版本号 String sign = getRequestParam(request, params[5]); // 验证签名 if (StringUtil.isEmpty(client) || StringUtil.isEmpty(version) || StringUtil.isEmpty(cuid) || StringUtil.isEmpty(format) || StringUtil.isEmpty(time) || StringUtil.isEmpty(sign)) { throw new BusinessException(BizExceptionEnum.SIGN_ERROR_1.getResponseCode(),BizExceptionEnum.SIGN_ERROR_1.getResponseMsg()); } if(DateUtil.between(new Date(Convert.toLong(time,0l)), new Date(), DateUnit.MINUTE) > 15) { throw new BusinessException(BizExceptionEnum.SIGN_ERROR_3.getResponseCode(),BizExceptionEnum.SIGN_ERROR_3.getResponseMsg()); } String bodyJsonStr = StrUtil.EMPTY; BodyReaderHttpServletRequestWrapper requestWrapper = null; if (!request.getRequestURI().contains("upload")) { try { requestWrapper = new BodyReaderHttpServletRequestWrapper(request); bodyJsonStr = requestWrapper.getRequestPostStr(requestWrapper); if(StrUtil.isBlank(bodyJsonStr)) { bodyJsonStr = StrUtil.EMPTY; } } catch (IOException e) { log.error("解析请求body异常",e); throw new BusinessException(BizExceptionEnum.SIGN_ERROR_2.getResponseCode(),BizExceptionEnum.SIGN_ERROR_2.getResponseMsg()); } } //拼接签名 StringBuilder signSource = new StringBuilder(); signSource.append(secret).append(client).append(cuid).append(format).append(time).append(version) .append(bodyJsonStr).append(secret); String keySign = MD5Implementor.MD5Encode(signSource.toString()).toLowerCase(); if (!StrUtil.equals(keySign,sign)) { log.error("接口{}签名验证失败,请求签名串:{},sign:{}",request.getRequestURL(), signSource, sign); throw new BusinessException(BizExceptionEnum.SIGN_ERROR.getResponseCode(),BizExceptionEnum.SIGN_ERROR.getResponseMsg()); } return requestWrapper; } /** * 从request中获取参数,先重head获取没有再从请求url上的参数中获取 * @param request * @param key * @return */ private String getRequestParam(HttpServletRequest request, String key) { String value = request.getHeader(key); if(StrUtil.isBlank(value)) { value = request.getParameter(key); } return value; } }
2022年04月09日
139 阅读
0 评论
0 点赞
2022-04-09
最近有人给我们提交了一个漏洞,涉及到泄露百万用户数据,然鹅我们APP利用AOP + Interceptor 完美避坑了...
项目开发中,我们不可避免的需要对外暴露接口,尤其是APP。往往我们需要对APP进行最基础的权限认证,防止接口越权等问题。然而最近有用户给我们提交了一个BUG, 说利用此BUG可以获取 所有用户 信息,一听到这个差点吓死,还好不是我们开发的项目,不过今天我们还是来复盘一下。1.这个漏洞是怎么出现的呢? 答:开放的接口中,直接上传用户id,然后后端代码根据上传的id查询用户信息并返回给前端,类似于这么一个接口/user/info?userId=1,前端登录后获取token,然后用postman直接调用接口,随便传任何用户id,都会返回用户id对应的用户信息。然鹅最要命的是这个用户id还是自增的,最要命的是这个接口返回的信息还是包含手机号、身份证信息等敏感信息的,最要命的是它返回的信息还是没有脱敏的。2.如何避免这个漏洞? 答:通过上面的解答,大家肯定已经想到了解决思路,第一就是当前用户只能访问当前用户的信息,且用户id不是直接由客户端传给服务端;第二就是用户id最好不好用自增主键;第三返回给前端的数据要做脱敏,简单的说比如你要展示手机号,后端给返回131**5037这种。3.咱代码怎么写? 答:我们不直接用前端上传的用户id,而是用用户登录信息中的id,然后比如查询订单的时候,用用户id+订单号查询,避免前面说到的系统漏洞。我也在下面写了一段示例代码,给大家参考。1.定义一个RequireLogin 注解@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequireLogin { }2.定义一个AuthorizationInterceptor类实现HandlerInterceptor@Component public class AuthorizationInterceptor implements HandlerInterceptor { @Autowired private JwtUtils jwtUtils; public static final String USER_KEY = "userId"; public static final String STORE_ID = "storeId"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { RequireLogin annotation; if (handler instanceof HandlerMethod) { annotation = ((HandlerMethod) handler).getMethodAnnotation(RequireLogin.class); } else { return true; } if (annotation == null) { return true; } //获取用户凭证 String token = request.getHeader(jwtUtils.getHeader()); if (StringUtils.isBlank(token)) { token = request.getParameter(jwtUtils.getHeader()); } // 获取storeId String storeId = request.getHeader(STORE_ID); //凭证为空 if (StringUtils.isBlank(token)) { throw new BaseException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value()); } if (StringUtils.isBlank(storeId)) { throw new BaseException(storeId + "不能为空。"); } Claims claims = jwtUtils.getClaimByToken(token); if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) { throw new BaseException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value()); } //设置userId到request里,后续根据userId,获取用户信息 request.setAttribute(USER_KEY, Integer.parseInt(claims.getSubject())); //设置storeId request.setAttribute(STORE_ID, Integer.parseInt(storeId)); return true; } } 3.通过WebMvcConfig注册我们添加的拦截器、方法参数解析器@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private AuthorizationInterceptor authorizationInterceptor; @Autowired private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authorizationInterceptor).addPathPatterns("/app/**"); } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(loginUserHandlerMethodArgumentResolver); } }4.controler添加注解实现需要登陆后访问 @ApiOperation("申请售后") @RequireLogin @PostMapping("apply") public Result apply(@RequestAttribute("userId") Integer userId, @RequestAttribute("storeId") Integer storeId, @RequestBody CommonForm<RefundApplyForm> form) { boolean apply = orderRefundService.apply(storeId, userId, form); return Result.ok(); }总结一下:系统的漏洞,往往都是前后端用户id这些传过来传过去,真正考虑安全的话,谢绝例如用户id直接从前端传。
2022年04月09日
73 阅读
0 评论
0 点赞
2022-04-09
利用参数解析器HandlerMethodArgumentResolver + AOP,实现注入当前登录用户信息
最近的项目,我们需要频繁的获取APP用户信息,我们可以利用参数解析器HandlerMethodArgumentResolver + AOP,实现注入当前登录用户信息。一:HandlerMethodArgumentResolver的介绍public interface HandlerMethodArgumentResolver { /** * 此解析器是否支持给定的方法参数。 * 参数:parameter - 要检查的方法参数 * 返回值: 如果此解析器支持提供的参数,则为true ;否则false */ boolean supportsParameter(MethodParameter parameter); /** * 将方法参数解析为给定请求的参数值。 ModelAndViewContainer提供对请求模型的访问。 WebDataBinderFactory提供了一种在需要进行数据绑定和类型转换时创建WeDataBinder实例的方法。 * 参数: * parameter -- 要解析的方法参数。此参数必须先前已传递给必须返回true的supportsParameter 。 * mavContainer – 当前请求的 ModelAndViewContainer * webRequest – 当前请求 * binderFactory – 用于创建WebDataBinder实例的工厂 * 返回值: * 解析的参数值,如果不可解析,则返回null * 抛出: * Exception ——如果参数值的准备出现错误 */ @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }简单点说:大家可以看到,这个接口有两个方法,supportsParameter和resolveArgument。方法supportsParameter很好理解,返回值是boolean类型,它的作用是判断Controller层中的参数,是否满足条件,满足条件则执行resolveArgument方法,不满足则跳过。而resolveArgument方法呢,它只有在supportsParameter方法返回true的情况下才会被调用。用于处理一些业务,将返回值赋值给Controller层中的这个参数。因此呢,我们可以将HandlerMethodArgumentResolver理解为是一个参数解析器,我们可以通过写一个类实现HandlerMethodArgumentResolver接口来实现对Controller层中方法参数的修改。二:真是项目实战2.1 定义一个用户信息(AOP)@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface LoginUser { }2.2 有@LoginUser注解的方法参数,注入当前登录用户@Component public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { @Autowired private UserService userService; @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterType().isAssignableFrom(UserEntity.class) && parameter.hasParameterAnnotation(LoginUser.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest request, WebDataBinderFactory factory) { //获取用户ID Object object = request.getAttribute(AuthorizationInterceptor.USER_KEY, RequestAttributes.SCOPE_REQUEST); if (object == null) { return null; } //获取用户信息,可以考虑一下缓存。 UserEntity user = userService.getById((Integer) object); return user; } }2.3 WebMvcConfig实现WebMvcConfigurer注册我们添加的拦截器、方法参数解析器等@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private AuthorizationInterceptor authorizationInterceptor; @Autowired private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authorizationInterceptor).addPathPatterns("/app/**"); } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(loginUserHandlerMethodArgumentResolver); } }2.4 controller 代码直接使用 @ApiOperation("购物车列表") @RequireLogin @GetMapping("/list") public Result cartList(@LoginUser UserEntity user) { CatListVo vo = cartService.cartList(user); return Result.ok().data(vo); }
2022年04月09日
70 阅读
0 评论
0 点赞
2022-03-30
聊聊SpringCache的使用(待完成...)
前言 在以前的编程中,我们如果去使用缓存,一般都是先通过缓存去取数据,如果没有数据,就去数据库查询,然后将查询到的数据缓存到redis中,以便后续直接去缓存中获取数据。这个过程很合理,但是却是极其重复性的工作,最近又看到SpringCache发现有了更简洁、优雅的实现方式。基础概念常用注解方法参数EL表达式一、引入依赖<!--引入springcache依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>二、配置文件# 配置使用cache类型 spring.cache.type=redis # 配置redis缓存有效期,不配置的话默认永久 spring.cache.redis.time-to-live=3600000 # 配置是否使用缓存前缀 spring.cache.redis.use-key-prefix=true # 配置缓存前缀,不指定则使用缓存的名字作为前缀 spring.cache.redis.key-prefix=cache: # 配置是否缓存空值,缓存空值可以避免缓存穿透 spring.cache.redis.cache-null-values=true # 配置是否开启缓存统计 spring.cache.redis.enable-statistics=false三、自定义cachemanager{callout color="#f0ad4e"}主要用于解决序列化等问题。{/callout}import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.cache.CacheProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; @EnableConfigurationProperties(CacheProperties.class) @Configuration @EnableCaching public class MyCacheConfig { // @Autowired // CacheProperties cacheProperties; /** * 配置文件中的东西没有用上 * * 1、原来和配置文件绑定的配置类是这样子的 * @ConfigurationProperties(prefix = "spring.cache") * public class CacheProperties * * 2、要让他生效 * @EnableConfigurationProperties(CacheProperties.class) * * @return */ @Bean RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); //将配置文件中的所有配置都让它生效 CacheProperties.Redis redisProperties = cacheProperties.getRedis(); if(redisProperties.getTimeToLive() != null){ config = config.entryTtl(redisProperties.getTimeToLive()); } if(redisProperties.getKeyPrefix() != null){ config = config.prefixKeysWith(redisProperties.getKeyPrefix()); } if(!redisProperties.isCacheNullValues()){ config = config.disableCachingNullValues(); } if(!redisProperties.isUseKeyPrefix()){ config = config.disableKeyPrefix(); } return config; } }三、使用示例3.1 删除缓存分区的数据 /** * 查询一级分类 * @return */ @Override @Cacheable(value = {"category"}, key = "'level1Categories'") public List<CategoryEntity> findLevel1Categories() { } /** * 查询全部分类 * 使用SpringCache缓存版本 * 只需要操作数据库,不需要关心缓存,一个注解就够了 * @return */ @Cacheable(value = {"category"}, key = "'categoryJson'") @Override public Map<Long, List<Category2VO>> getCategoryJson() { // 查出所有分类 List<CategoryEntity> allCategories = this.list(); List<CategoryEntity> l1Categories = listByPrentCid(allCategories, 0L); Map<Long, List<Category2VO>> categoryMap = l1Categories.stream().collect(Collectors.toMap(k1 -> k1.getCatId(), v1 -> { List<CategoryEntity> l2Categories = listByPrentCid(allCategories, v1.getCatId()); List<Category2VO> category2VOs = null; if (l2Categories != null && l2Categories.size() > 0) { category2VOs = l2Categories.stream().map(l2 -> { // 根据当前2级分类查出所有3级分类 List<CategoryEntity> l3Categories = listByPrentCid(allCategories, l2.getCatId()); List<Category3VO> category3VOs = null; if (l3Categories != null && l3Categories.size() > 0) { category3VOs = l3Categories.stream().map(l3 -> new Category3VO(l2.getCatId(), l3.getCatId(), l3.getName())).collect(Collectors.toList()); } return new Category2VO(v1.getCatId(), category3VOs, l2.getCatId(), l2.getName()); }).collect(Collectors.toList()); } return category2VOs; })); return categoryMap; }3.1.2 删除一个分区的缓存数据 // 删除一个分区的缓存数据 @CacheEvict(value = "category", key = "'level1Categories'") public void updateDetail(CategoryEntity category) { }3.1.3 删除多个分区的缓存数据// 删除多个分区的缓存数据 @Caching(evict = { @CacheEvict(value = "category", key = "'level1Categories'"), @CacheEvict(value = "category", key = "'categoryJson'"), }) 3.1.4 删除指定分区的缓存数据// 删除某个分区下的缓存数据,也就是清除模式 @CacheEvict(value = "category",allEntries = true) // 如果希望修改完数据,再往缓存里放一份,也就是双写模式,可以使用这个注解 @CachePut
2022年03月30日
108 阅读
0 评论
0 点赞
2022-03-29
Redis实现秒杀
思路如果没有库存,则说明活动还未开始如果库存数量小于1,则说明活动已结束如果秒杀成功列表中包含当前用户,则说明该用户已成功参与,不允许多次参与步骤拼接库存和秒杀成功的key判断当前用户是否已经参与获取库存数量,判断活动是否开始,或者是已结束进行减库存和添加用户操作Java 功能代码@Autowired private RedisTemplate<String, Object> redisTemplate; public boolean doSecondKill(String uid, String goodsId) { // 1. 参数校验 if( StringUtils.isEmpty(uid) || StringUtils.isEmpty(goodsId) ) { return false; } // 2. 获取Redis操作对象 ValueOperations<String, Object> strOps =redisTemplate.opsForValue(); SetOperations<String, Object> setOps = redisTemplate.opsForSet(); // 3. 拼接key // 3.1 库存key String stockKey = "SK:"+goodsId+":nums"; // 3.2 秒杀用户成功key String userKey = "SK:" + goodsId + ":users"; // 4. 获取库存,如果库存为空,秒杀还未开始 Object stock = strOps.get(stockKey); if(StringUtils.isEmpty(stock)) { System.out.println("秒杀还未开始,请等待……"); return false; } // 5. 判断用户是否重复秒杀操作 if(setOps.isMember(userKey, uid)) { System.out.println("已参与"); return false; } // 6. 判断商品数量,如果商品数量小于1,秒杀结束 if(Integer.parseInt(stock.toString()) < 1) { System.out.println("秒杀已结束"); return false; } // 7. 秒杀 // 7.1 库存减一 strOps.decrement(stockKey); // 7.2 秒杀成功的用户添加清单里面 setOps.add(userKey, uid); return true; }存在问题 - 超卖问题{dotted startColor="#ff6c6c" endColor="#1989fa"/}解决超卖问题 - Redis事务public boolean doSecondKill(String uid, String goodsId) { // 1. 参数校验 if( StringUtils.isEmpty(uid) || StringUtils.isEmpty(goodsId) ) { return false; } // 2. 获取Redis操作对象 ValueOperations<String, Object> strOps =redisTemplate.opsForValue(); SetOperations<String, Object> setOps = redisTemplate.opsForSet(); // 3. 拼接key // 3.1 库存key String stockKey = "SK:"+goodsId+":nums"; // 3.2 秒杀用户成功key String userKey = "SK:" + goodsId + ":users"; redisTemplate.watch(stockKey); // 4. 获取库存,如果库存为空,秒杀还未开始 Object stock = strOps.get(stockKey); if(StringUtils.isEmpty(stock)) { System.out.println("秒杀还未开始,请等待……"); return false; } // 5. 判断用户是否重复秒杀操作 if(setOps.isMember(userKey, uid)) { System.out.println("已参与"); return false; } // 6. 判断商品数量,如果商品数量小于1,秒杀结束 if(Integer.parseInt(stock.toString()) < 1) { System.out.println("秒杀已结束"); return false; } // 7. 秒杀 redisTemplate.multi(); // 7.1 库存减一 strOps.decrement(stockKey); // 7.2 秒杀成功的用户添加清单里面 setOps.add(userKey, uid); List<Object> execs = redisTemplate.exec(); if(null == execs || execs.isEmpty()) { System.out.println("秒杀失败"); return false; } return true; }存在问题 - 库存遗留问题{dotted startColor="#ff6c6c" endColor="#1989fa"/}解决库存遗留问题 - 锁机制 + Luaprivate void lock(String lockKey, String lockValue, String uid) { Boolean nativeLock = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, Duration.ofSeconds(30)); System.out.println(uid + "->" + "加锁状态:" + nativeLock); if(nativeLock) { // 加锁成功 try { // 业务代码 // ... 此处省略 } catch (InterruptedException e) { e.printStackTrace(); } finally { // 解锁 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) else return 0 end"; Integer ret = redisTemplate.execute(new DefaultRedisScript<Integer>(script, Integer.class), Arrays.asList(lockKey), lockValue); System.out.println(uid + "->" + "解锁:" + "\t\t" + ret); } } else { // 自旋锁 System.out.println(uid + "->" + "加锁失败,睡眠100ms "); try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } lock(lockKey, lockValue, uid); } }存在问题 - 超时{dotted startColor="#ff6c6c" endColor="#1989fa"/}解决超时问题 - redisson + Lualocal stockKey = KEYS[1]; local userKey = KEYS[2]; local userId = ARGV[1]; -- 判断用户是否已参与 local userExists=redis.call("sismember", userKey, userId); if type(userExists) == "number" and tonumber(userExists)==1 then -- 已参与 return 2; end -- 库存判断 & 减库存操作 local num = redis.call("get", stockKey); if type(num) == "boolean" then -- 还未开始 return -1; elseif type(num) == "number" and tonumber(num) <= 0 then -- 秒杀结束 return 0; else redis.call("decr", stockKey); redis.call("sadd", userKey, userId); end -- 秒杀成功 return 1; private Boolean redissonLock(String lockName, String uid, String goodsId) { RLock lock = redissonClient.getLock(lockName); lock.lock(); try { DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/sk.lua"))); redisScript.setResultType(Long.class); String stockKey = "SK:" + goodsId + ":stocks"; String userKey = "SK:" + goodsId + ":users"; // System.out.println(stockKey); // System.out.println(userKey); Long ret = redisTemplate.execute(redisScript, Arrays.asList(stockKey, userKey), uid); System.out.println(ret); return ret == 1; } finally { lock.unlock(); } }
2022年03月29日
64 阅读
0 评论
0 点赞
2022-03-29
开源一个OSS文件上传的starter项目【bdysoft-oss-starter】
介绍一款支持七牛云、腾讯云、阿里云文件上传的starter安装说明下载代码后,执行 mvn clean install 安装到本地仓库使用说明在pom.xml 添加依赖<dependency> <groupId>com.bdysoft</groupId> <artifactId>shop-cloud-starter-oss</artifactId> <version>1.0-SNAPSHOT</version> </dependency>2.在yaml文件中配置参数oss: type: qiniu-access-key: qiniu-secret-key: qiniu-domain: qiniu-bucket-name: qiniu-prefix: 3.在代码中直接使用文件上传// 获取文件原始名 String originalFilename = file.getOriginalFilename(); // 获取文件后缀名 String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); // 文件上传 CloudStorageUploadResult uploadResult = cloudStorageService.build().uploadSuffix(file.getBytes(), suffix);项目主页https://gitee.com/bdysoft_admin/bdysoft-oss-starter
2022年03月29日
73 阅读
0 评论
0 点赞
2022-03-29
神器 SpringDoc 横空出世!最适合 SpringBoot 的API文档工具来了
之前在SpringBoot项目中一直使用的是SpringFox提供的Swagger库,上了下官网发现已经有接近两年没出新版本了!前几天升级了SpringBoot 2.6.x 版本,发现这个库的兼容性也越来越不好了,有的常用注解属性被废弃了居然都没提供替代!无意中发现了另一款Swagger库SpringDoc,试用了一下非常不错,推荐给大家。呃,实际项目中用不用呢?咱说了也不算,不过可以收藏一下,后面备查吧。SpringDoc简介 SpringDoc是一款可以结合SpringBoot使用的API文档生成工具,基于OpenAPI 3,目前在Github上已有1.7K+Star,更新发版还是挺勤快的,是一款更好用的Swagger库!值得一提的是SpringDoc不仅支持Spring WebMvc项目,还可以支持Spring WebFlux项目,甚至Spring Rest和Spring Native项目,总之非常强大,下面是一张SpringDoc的架构图。 使用接下来我们介绍下SpringDoc的使用,使用的是之前集成SpringFox的mall-tiny-swagger项目,我将把它改造成使用SpringDoc。集成首先我们得集成SpringDoc,在pom.xml中添加它的依赖即可,开箱即用,无需任何配置。<!--springdoc 官方Starter--> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.6.6</version> </dependency>从SpringFox迁移我们先来看下经常使用的Swagger注解,看看SpringFox的和SpringDoc的有啥区别,毕竟对比已学过的技术能该快掌握新技术;接下来我们对之前Controller中使用的注解进行改造,对照上表即可,之前在@Api注解中被废弃了好久又没有替代的description属性终于被支持了!@Tag(name = "PmsBrandController", description = "商品品牌管理") @Controller @RequestMapping("/brand") public class PmsBrandController { @Autowired private PmsBrandService brandService; private static final Logger LOGGER = LoggerFactory.getLogger(PmsBrandController.class); @Operation(summary = "获取所有品牌列表",description = "需要登录后访问") @RequestMapping(value = "listAll", method = RequestMethod.GET) @ResponseBody public CommonResult<List<PmsBrand>> getBrandList() { return CommonResult.success(brandService.listAllBrand()); } @Operation(summary = "添加品牌") @RequestMapping(value = "/create", method = RequestMethod.POST) @ResponseBody @PreAuthorize("hasRole('ADMIN')") public CommonResult createBrand(@RequestBody PmsBrand pmsBrand) { CommonResult commonResult; int count = brandService.createBrand(pmsBrand); if (count == 1) { commonResult = CommonResult.success(pmsBrand); LOGGER.debug("createBrand success:{}", pmsBrand); } else { commonResult = CommonResult.failed("操作失败"); LOGGER.debug("createBrand failed:{}", pmsBrand); } return commonResult; } @Operation(summary = "更新指定id品牌信息") @RequestMapping(value = "/update/{id}", method = RequestMethod.POST) @ResponseBody @PreAuthorize("hasRole('ADMIN')") public CommonResult updateBrand(@PathVariable("id") Long id, @RequestBody PmsBrand pmsBrandDto, BindingResult result) { CommonResult commonResult; int count = brandService.updateBrand(id, pmsBrandDto); if (count == 1) { commonResult = CommonResult.success(pmsBrandDto); LOGGER.debug("updateBrand success:{}", pmsBrandDto); } else { commonResult = CommonResult.failed("操作失败"); LOGGER.debug("updateBrand failed:{}", pmsBrandDto); } return commonResult; } @Operation(summary = "删除指定id的品牌") @RequestMapping(value = "/delete/{id}", method = RequestMethod.GET) @ResponseBody @PreAuthorize("hasRole('ADMIN')") public CommonResult deleteBrand(@PathVariable("id") Long id) { int count = brandService.deleteBrand(id); if (count == 1) { LOGGER.debug("deleteBrand success :id={}", id); return CommonResult.success(null); } else { LOGGER.debug("deleteBrand failed :id={}", id); return CommonResult.failed("操作失败"); } } @Operation(summary = "分页查询品牌列表") @RequestMapping(value = "/list", method = RequestMethod.GET) @ResponseBody @PreAuthorize("hasRole('ADMIN')") public CommonResult<CommonPage<PmsBrand>> listBrand(@RequestParam(value = "pageNum", defaultValue = "1") @Parameter(description = "页码") Integer pageNum, @RequestParam(value = "pageSize", defaultValue = "3") @Parameter(description = "每页数量") Integer pageSize) { List<PmsBrand> brandList = brandService.listBrand(pageNum, pageSize); return CommonResult.success(CommonPage.restPage(brandList)); } @Operation(summary = "获取指定id的品牌详情") @RequestMapping(value = "/{id}", method = RequestMethod.GET) @ResponseBody @PreAuthorize("hasRole('ADMIN')") public CommonResult<PmsBrand> brand(@PathVariable("id") Long id) { return CommonResult.success(brandService.getBrand(id)); } }接下来进行SpringDoc的配置,使用OpenAPI来配置基础的文档信息,通过GroupedOpenApi配置分组的API文档,SpringDoc支持直接使用接口路径进行配置。@Configuration public class SpringDocConfig { @Bean public OpenAPI mallTinyOpenAPI() { return new OpenAPI() .info(new Info().title("Mall-Tiny API") .description("SpringDoc API 演示") .version("v1.0.0") .license(new License().name("Apache 2.0").url("https://github.com/macrozheng/mall-learning"))) .externalDocs(new ExternalDocumentation() .description("SpringBoot实战电商项目mall(50K+Star)全套文档") .url("http://www.macrozheng.com")); } @Bean public GroupedOpenApi publicApi() { return GroupedOpenApi.builder() .group("brand") .pathsToMatch("/brand/**") .build(); } @Bean public GroupedOpenApi adminApi() { return GroupedOpenApi.builder() .group("admin") .pathsToMatch("/admin/**") .build(); } }结合SpringSecurity使用由于我们的项目集成了SpringSecurity,需要通过JWT认证头进行访问,我们还需配置好SpringDoc的白名单路径,主要是Swagger的资源路径;@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf .disable() .sessionManagement()// 基于token,所以不需要session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(HttpMethod.GET, // Swagger的资源路径需要允许访问 "/", "/swagger-ui.html", "/swagger-ui/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js", "/swagger-resources/**", "/v3/api-docs/**" ) .permitAll() .antMatchers("/admin/login")// 对登录注册要允许匿名访问 .permitAll() .antMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求 .permitAll() .anyRequest()// 除上面外的所有请求全部需要鉴权认证 .authenticated(); } }然后在OpenAPI对象中通过addSecurityItem方法和SecurityScheme对象,启用基于JWT的认证功能。@Configuration public class SpringDocConfig { private static final String SECURITY_SCHEME_NAME = "BearerAuth"; @Bean public OpenAPI mallTinyOpenAPI() { return new OpenAPI() .info(new Info().title("Mall-Tiny API") .description("SpringDoc API 演示") .version("v1.0.0") .license(new License().name("Apache 2.0").url("https://github.com/macrozheng/mall-learning"))) .externalDocs(new ExternalDocumentation() .description("SpringBoot实战电商项目mall(50K+Star)全套文档") .url("http://www.macrozheng.com")) .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) .components(new Components() .addSecuritySchemes(SECURITY_SCHEME_NAME, new SecurityScheme() .name(SECURITY_SCHEME_NAME) .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))); } }测试接下来启动项目就可以访问Swagger界面了,访问地址:http://localhost:8088/swagger-ui.html我们先通过登录接口进行登录,可以发现这个版本的Swagger返回结果是支持高亮显示的,版本明显比SpringFox来的新;然后通过认证按钮输入获取到的认证头信息,注意这里不用加bearer前缀;之后我们就可以愉快地访问需要登录认证的接口了;看一眼请求参数的文档说明,还是熟悉的Swagger样式!常用配置SpringDoc还有一些常用的配置可以了解下,更多配置可以参考官方文档。springdoc: swagger-ui: # 修改Swagger UI路径 path: /swagger-ui.html # 开启Swagger UI界面 enabled: true api-docs: # 修改api-docs路径 path: /v3/api-docs # 开启api-docs enabled: true # 配置需要生成接口文档的扫描包 packages-to-scan: com.macro.mall.tiny.controller # 配置需要生成接口文档的接口路径 paths-to-match: /brand/**,/admin/**总结 在SpringFox的Swagger库好久不出新版的情况下,迁移到SpringDoc确实是一个更好的选择。今天体验了一把SpringDoc,确实很好用,和之前熟悉的用法差不多,学习成本极低。而且SpringDoc能支持WebFlux之类的项目,功能也更加强大,使用SpringFox有点卡手的朋友可以迁移到它试试!
2022年03月29日
63 阅读
0 评论
0 点赞
2022-03-29
7种方式,教你提升 Spring Boot 项目的吞吐量
一、异步执行实现方式二种:使用异步注解@aysnc、启动类:添加 @EnableAsync 注解JDK 8本身有一个非常好用的Future类— —CompletableFuture@AllArgsConstructor public class AskThread implements Runnable{ private CompletableFuture<Integer> re = null; public void run() { int myRe = 0; try { myRe = re.get() * re.get(); } catch (Exception e) { e.printStackTrace(); } System.out.println(myRe); } public static void main(String[] args) throws InterruptedException { final CompletableFuture<Integer> future = new CompletableFuture<>(); new Thread(new AskThread(future)).start(); //模拟长时间的计算过程 Thread.sleep(1000); //告知完成结果 future.complete(60); } } 在该示例中,启动一个线程,此时AskThread对象还没有拿到它需要的数据,执行到 myRe = re.get() * re.get()会阻塞。我们用休眠1秒来模拟一个长时间的计算过程,并将计算结果告诉future执行结果,AskThread线程将会继续执行。public class Calc { public static Integer calc(Integer para) { try { //模拟一个长时间的执行 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return para * para; } public static void main(String[] args) throws ExecutionException, InterruptedException { final CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> calc(50)) .thenApply((i) -> Integer.toString(i)) .thenApply((str) -> "\"" + str + "\"") .thenAccept(System.out::println); future.get(); } } CompletableFuture.supplyAsync方法构造一个CompletableFuture实例,在supplyAsync()方法中,它会在一个新线程中,执行传入的参数。在这里它会执行calc()方法,这个方法可能是比较慢的,但这并不影响CompletableFuture实例的构造速度,supplyAsync()会立即返回。而返回的CompletableFuture实例就可以作为这次调用的契约,在将来任何场合,用于获得最终的计算结果。 supplyAsync用于提供返回值的情况,CompletableFuture还有一个不需要返回值的异步调用方法runAsync(Runnable runnable),一般我们在优化Controller时,使用这个方法比较多。这两个方法如果在不指定线程池的情况下,都是在ForkJoinPool.common线程池中执行,而这个线程池中的所有线程都是Daemon(守护)线程,所以,当主线程结束时,这些线程无论执行完毕都会退出系统。CompletableFuture.runAsync(() -> this.afterBetProcessor(betRequest,betDetailResult,appUser,id) );异步调用使用Callable来实现@RestController public class HelloController { private static final Logger logger = LoggerFactory.getLogger(HelloController.class); @Autowired private HelloService hello; @GetMapping("/helloworld") public String helloWorldController() { return hello.sayHello(); } /** * 异步调用restful * 当controller返回值是Callable的时候,springmvc就会启动一个线程将Callable交给TaskExecutor去处理 * 然后DispatcherServlet还有所有的spring拦截器都退出主线程,然后把response保持打开的状态 * 当Callable执行结束之后,springmvc就会重新启动分配一个request请求,然后DispatcherServlet就重新 * 调用和处理Callable异步执行的返回结果, 然后返回视图 * * @return */ @GetMapping("/hello") public Callable<String> helloController() { logger.info(Thread.currentThread().getName() + " 进入helloController方法"); Callable<String> callable = new Callable<String>() { @Override public String call() throws Exception { logger.info(Thread.currentThread().getName() + " 进入call方法"); String say = hello.sayHello(); logger.info(Thread.currentThread().getName() + " 从helloService方法返回"); return say; } }; logger.info(Thread.currentThread().getName() + " 从helloController方法返回"); return callable; } } 异步调用的方式 WebAsyncTask@RestController public class HelloController { private static final Logger logger = LoggerFactory.getLogger(HelloController.class); @Autowired private HelloService hello; /** * 带超时时间的异步请求 通过WebAsyncTask自定义客户端超时间 * * @return */ @GetMapping("/world") public WebAsyncTask<String> worldController() { logger.info(Thread.currentThread().getName() + " 进入helloController方法"); // 3s钟没返回,则认为超时 WebAsyncTask<String> webAsyncTask = new WebAsyncTask<>(3000, new Callable<String>() { @Override public String call() throws Exception { logger.info(Thread.currentThread().getName() + " 进入call方法"); String say = hello.sayHello(); logger.info(Thread.currentThread().getName() + " 从helloService方法返回"); return say; } }); logger.info(Thread.currentThread().getName() + " 从helloController方法返回"); webAsyncTask.onCompletion(new Runnable() { @Override public void run() { logger.info(Thread.currentThread().getName() + " 执行完毕"); } }); webAsyncTask.onTimeout(new Callable<String>() { @Override public String call() throws Exception { logger.info(Thread.currentThread().getName() + " onTimeout"); // 超时的时候,直接抛异常,让外层统一处理超时异常 throw new TimeoutException("调用超时"); } }); return webAsyncTask; } /** * 异步调用,异常处理,详细的处理流程见MyExceptionHandler类 * * @return */ @GetMapping("/exception") public WebAsyncTask<String> exceptionController() { logger.info(Thread.currentThread().getName() + " 进入helloController方法"); Callable<String> callable = new Callable<String>() { @Override public String call() throws Exception { logger.info(Thread.currentThread().getName() + " 进入call方法"); throw new TimeoutException("调用超时!"); } }; logger.info(Thread.currentThread().getName() + " 从helloController方法返回"); return new WebAsyncTask<>(20000, callable); } } 二、增加内嵌Tomcat的最大连接数@Configuration public class TomcatConfig { @Bean public ConfigurableServletWebServerFactory webServerFactory() { TomcatServletWebServerFactory tomcatFactory = new TomcatServletWebServerFactory(); tomcatFactory.addConnectorCustomizers(new MyTomcatConnectorCustomizer()); tomcatFactory.setPort(8005); tomcatFactory.setContextPath("/api-g"); return tomcatFactory; } class MyTomcatConnectorCustomizer implements TomcatConnectorCustomizer { public void customize(Connector connector) { Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler(); //设置最大连接数 protocol.setMaxConnections(20000); //设置最大线程数 protocol.setMaxThreads(2000); protocol.setConnectionTimeout(30000); } } }三、使用@ComponentScan()定位扫包比@SpringBootApplication扫包更快四、默认tomcat容器改为Undertow(Jboss下的服务器,Tomcat吞吐量5000,Undertow吞吐量8000)<exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>五、使用 BufferedWriter 进行缓冲六、Deferred方式实现异步调用@RestController public class AsyncDeferredController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final LongTimeTask taskService; @Autowired public AsyncDeferredController(LongTimeTask taskService) { this.taskService = taskService; } @GetMapping("/deferred") public DeferredResult<String> executeSlowTask() { logger.info(Thread.currentThread().getName() + "进入executeSlowTask方法"); DeferredResult<String> deferredResult = new DeferredResult<>(); // 调用长时间执行任务 taskService.execute(deferredResult); // 当长时间任务中使用deferred.setResult("world");这个方法时,会从长时间任务中返回,继续controller里面的流程 logger.info(Thread.currentThread().getName() + "从executeSlowTask方法返回"); // 超时的回调方法 deferredResult.onTimeout(new Runnable(){ @Override public void run() { logger.info(Thread.currentThread().getName() + " onTimeout"); // 返回超时信息 deferredResult.setErrorResult("time out!"); } }); // 处理完成的回调方法,无论是超时还是处理成功,都会进入这个回调方法 deferredResult.onCompletion(new Runnable(){ @Override public void run() { logger.info(Thread.currentThread().getName() + " onCompletion"); } }); return deferredResult; } }七、异步调用可以使用AsyncHandlerInterceptor进行拦截@Component public class MyAsyncHandlerInterceptor implements AsyncHandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(MyAsyncHandlerInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // HandlerMethod handlerMethod = (HandlerMethod) handler; logger.info(Thread.currentThread().getName()+ "服务调用完成,返回结果给客户端"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { if(null != ex){ System.out.println("发生异常:"+ex.getMessage()); } } @Override public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 拦截之后,重新写回数据,将原来的hello world换成如下字符串 String resp = "my name is chhliu!"; response.setContentLength(resp.length()); response.getOutputStream().write(resp.getBytes()); logger.info(Thread.currentThread().getName() + " 进入afterConcurrentHandlingStarted方法"); } }
2022年03月29日
48 阅读
0 评论
0 点赞
2022-03-29
优雅的SpringBoot自定义异常捕获
一、 前言 在日常项目开发中,异常是常见的,但是如何更高效的处理好异常信息,让我们能快速定位到BUG,是很重要的,不仅能够提高我们的开发效率,还能让你代码看上去更舒服,SpringBoot的项目已经对有一定的异常处理了,但是对于我们开发者而言可能就不太合适了,因此我们需要对这些异常进行统一的捕获并处理。 如果不进行异常处理,当出现错误的时候,返回的信息可能是如下图这样的:二、异常分类1. 从定义角度的异常分类(1). Error(错误) 程序在执行过程中所遇到的硬件或操作系统的错误。错误对程序而言是致命的,将导致程序无法运行。常见的错误有内存溢出,jvm 虚拟机自身的非正常运行,calss 文件没有主方法。程序本生是不能处理错误的,只能依靠外界干预。Error 是系统内部的错误,由 jvm 抛出,交给系统来处理。(2). Exception(错误) 是程序正常运行中,可以预料的意外情况。比如数据库连接中断,空指针,数组下标越界。异常出现可以导致程序非正常终止,也可以预先检测,被捕获处理掉,使程序继续运行。 EXCEPTION(异常)按照性质,又分为编译异常(可检测)和运行时异常(不可检测)。a.编译时异常 又叫可检查异常,通常时由语法错和环境因素(外部资源)造成的异常。比如输入输出异常 IOException,数据库操作 SQLException。其特点是,Java 语言强制要求捕获和处理所有非运行时异常。通过行为规范,强化程序的健壮性和安全性。b.运行时异常 又叫不检查异常 RuntimeException,这些异常一般是由程序逻辑错误引起的,即语义错。比如算术异常,空指针异常 NullPointerException,下标越界 IndexOutOfBoundsException。运行时异常应该在程序测试期间被暴露出来,由程序员去调试,而避免捕获。2. 从开发的角度对异常分类我们也可以把异常分类 已知异常 和 未知异常 。(1). 已知异常 代表我们可以控制的异常,比如当一个资源没找到的时候,要返回给前端,抛出资源没有找到。 当校验参数的时候,参数缺失,抛出参数缺失给前端。(2). 未知异常 代表我们也不知道什么时候程序可能会报错,可能某个地方判断不严谨,导致空指针或下标越界等。三、如何优雅的进行异常捕获(重点是已知异常的处理) 我们知道,使用springboot的时候,我们可以使用@ControllerAdvice和@ExceptionHandler对异常进行全局异常捕获,但如何做,才能使异常捕获更加的优雅呢?我们可以从已知异常和未知异常进行自定义的异常处理。1. 未知异常的处理1.新建一个GlobalExceptionAdvice全局异常处理的类,加上@ControllerAdvice注解2.捕获Exception.class @ExceptionHandler(Exception.class) @ResponseBody @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR) public ReturnMsgEntity handlerException(HttpServletRequest request, Exception e) { String uri = request.getRequestURI(); String method = request.getMethod(); ReturnMsgEntity message = new ReturnMsgEntity(9999, codeConfiguration.getMessage(9999), null, method + " " + uri); return message; }解释 为了防止硬编码,写了一个codeConfiguration配置类,作用是读取某个路径下的文件,封装成一个map@Component @ConfigurationProperties(prefix="aisino") @PropertySource("classpath:config/exception-code.properties") public class ExceptionCodeConfiguration { private Map<Integer,String> codes = new HashMap<>(); public String getMessage(int code){ return codes.get(code); } public Map<Integer, String> getCodes() { return codes; } public void setCodes(Map<Integer, String> codes) { this.codes = codes; } }exception-code.properties配置文件如下:aisino.codes[9999] = 服务器未知异常 aisino.codes[10000] = 通用异常 aisino.codes[10001] = 通用参数错误 aisino.codes[0] = ok aisino.codes[10002] = 资源未找到以上对于未知异常的处理就完事了,看看效果,模拟一个空指针异常的接口,访问结果如下:2.已知异常的处理(重点) 已知异常有很多种,比如禁止访问,参数缺失,没有找到资源,没有认证,难道我们要把每个错误都写在GlobalExceptionAdvice类中吗?这样写是不是不容易维护,比如当资源不存在该如何抛出呢? 针对以上问题,可以在GlobalExceptionAdvice中捕获一个自己定义的HttpException,其他的错误类都HttpException,HttpException又继承RuntimeException类,这样我们就可以实现抛出自己定义的错误了,具体的HttpException类如下:public class HttpException extends RuntimeException{ //自定义错误码 protected Integer code; // 默认的状态码 protected Integer httpStatusCode = 500; public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public Integer getHttpStatusCode() { return httpStatusCode; } public void setHttpStatusCode(Integer httpStatusCode) { this.httpStatusCode = httpStatusCode; } }我们还需要定义其他具体的子类,继承上面的类,比如NotFoundException,如下代码:public class NotFoundException extends HttpException { public NotFoundException(int code){ this.httpStatusCode = 404; this.code = code; } }在GlobalExceptionAdvice中就可以捕获HttpException类了,代码如下: @ExceptionHandler(HttpException.class) @ResponseBody public ResponseEntity<ReturnMsgEntity> handlerHttpException(HttpServletRequest request, HttpException e) { // 获得请求路径 String requestUrl = request.getRequestURI(); // 获得请求方法 String method = request.getMethod(); // e.getcode 根据我们自定义的状态码获取具体的错误信息 ReturnMsgEntity message = new ReturnMsgEntity(e.getCode(), codeConfiguration.getMessage(e.getCode()), null, method + " " + requestUrl); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); // 设置我们自定义的http状态码,比如404对应资源没找到,前端收到的状态码不是200,是404了 HttpStatus httpStatus = HttpStatus.resolve(e.getHttpStatusCode()); ResponseEntity<ReturnMsgEntity> responseEntity = new ResponseEntity<>(message, headers, httpStatus); return responseEntity; }最后我们使用一下,比如:当查不到用户信息的时候,返回信息如下:
2022年03月29日
138 阅读
0 评论
0 点赞
2022-03-28
玩转SpringBoot之定时任务@Scheduled线程池配置
序言 对于定时任务,在SpringBoot中只需要使用@Scheduled 这个注解就能够满足需求,它的出现也给我们带了很大的方便,我们只要加上该注解,并且根据需求设置好就可以使用定时任务了。但是,我们需要注意的是,@Scheduled 并不一定会按时执行。因为使用@Scheduled 的定时任务虽然是异步执行的,但是,不同的定时任务之间并不是并行的!!!!!!!! 在其中一个定时任务没有执行完之前,其他的定时任务即使是到了执行时间,也是不会执行的,它们会进行排队。也就是如果你想你不同的定时任务互不影响,到时间就会执行,那么你最好将你的定时任务方法自己搞成异步方法,这样,定时任务其实就相当于调用了一个线程执行任务,一瞬间就结束了。比如使用:@Async 当然,也可以勉强将你的定时任务当做都会定时执行。但是,作为一个合格的程序员那么,如何将@Scheduled实现的定时任务变成异步的呢?此时你需要对@Scheduled进行线程池配置。配置示例package com.java.navtool.business.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; /** * @description:spring-boot 多线程 @Scheduled注解 并发定时任务的解决方案 */ @Configuration @EnableScheduling public class ScheduleConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(taskExecutor()); } public static final String EXECUTOR_SERVICE = "scheduledExecutor"; @Bean(EXECUTOR_SERVICE) public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 设置核心线程数 executor.setCorePoolSize(Runtime.getRuntime().availableProcessors()); // 设置最大线程数 executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 10); // 设置队列容量 executor.setQueueCapacity(Runtime.getRuntime().availableProcessors() * 10); // 设置线程活跃时间(秒) executor.setKeepAliveSeconds(10); // 设置默认线程名称 executor.setThreadNamePrefix("scheduled-"); // 设置拒绝策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 等待所有任务结束后再关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); return executor; } }附带介绍一下线程池的几个参数。需要彻底搞懂,不要死记硬背哦!{dotted startColor="#ff6c6c" endColor="#1989fa"/}线程池参数1、corePoolSize(必填):核心线程数。2、maximumPoolSize(必填):最大线程数。3、keepAliveTime(必填):线程空闲时长。如果超过该时长,非核心线程就会被回收。4、unit(必填):指定keepAliveTime的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。5、workQueue(必填):任务队列。通过线程池的execute()方法提交的Runnable对象将存储在该队列中。6、threadFactory(可选):线程工厂。一般就用默认的。7、handler(可选):拒绝策略。当线程数达到最大线程数时就要执行饱和策略。{dotted startColor="#ff6c6c" endColor="#1989fa"/}说下核心线程数和最大线程数的区别:拒绝策略可选值:1、AbortPolicy(默认):放弃任务并抛出RejectedExecutionException异常。2、CallerRunsPolicy:由调用线程处理该任务。3、DiscardPolicy:放弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。4、DiscardOldestPolicy:放弃队列最早的未处理任务,然后重新尝试执行任务。{dotted startColor="#ff6c6c" endColor="#1989fa"/}线程池执行流程:{dotted startColor="#ff6c6c" endColor="#1989fa"/}玩转SpringBoot之定时任务@Scheduled线程池配置简短的总结下线程池执行流程:1、一个任务提交到线程池后,如果当前的线程数没达到核心线程数,则新建一个线程并且执行新任务,注意一点,这个新任务执行完后,该线程不会被销毁;2、如果达到了,则判断任务队列满了没,如果没满,则将任务放入任务队列;3、如果满了,则判断当前线程数量是否达到最大线程数,如果没达到,则创建新线程来执行任务,注意,如果线程池中线程数量大于核心线程数,每当有线程超过了空闲时间,就会被销毁,直到线程数量不大于核心线程数;4、如果达到了最大线程数,并且任务队列满了,就会执行饱和策略;
2022年03月28日
81 阅读
0 评论
0 点赞
2022-03-24
微服务实现统一认证和鉴权
微服务实现统一认证和鉴权在我们的电商系统开发过程中,无论是管理后台还是APP客户端都需要将请求发送至网关,由网关来进行分发,但是在这过程中可能会遇到非法的请求或者权限越界等行为,鉴于此,我们需要在网关层就实现统一的认证与鉴权,研究了一天左右,终于在自己的项目中实现了网关层的鉴权,分享经验给大家。一、搭建认证服务器1.1 引入pom依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.2.5.RELEASE</version> </dependency> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>8.16</version> </dependency>1.2 修改yml文件spring: datasource: password: username: url: redis: database: 0 port: 6379 host: localhost oauth2: jwtPassword: 123456 authorizedGrantTypes: - "password" - "refresh_token" scopes: all secret: 123456 accessTokenValiditySeconds: 3600 # 配置访问token的有效期 refreshTokenValiditySeconds: 3600 # 配置刷新token的有效期1.3 正式编码1.3.1 定义权限相关的常量package com.bdysoft.shop.auth.constant; /** * 权限相关的常量 * * @author lvwei */ public class AuthConstant { /** * JWT存储权限前缀 */ public static final String AUTHORITY_PREFIX = "ROLE_"; /** * JWT存储权限属性 */ public static final String AUTHORITY_CLAIM_NAME = "authorities"; /** * 后台管理client_id */ public static final String ADMIN_CLIENT_ID = "shop-admin"; /** * 前台商城client_id */ public static final String PORTAL_CLIENT_ID = "shop-app"; /** * 后台管理接口路径匹配 */ public static final String ADMIN_URL_PATTERN = "/shop-admin/**"; /** * Redis缓存权限规则key */ public static final String RESOURCE_ROLES_MAP_KEY = "auth:resourceRolesMap"; /** * 认证信息Http请求头 */ public static final String JWT_TOKEN_HEADER = "Authorization"; /** * JWT令牌前缀 */ public static final String JWT_TOKEN_PREFIX = "Shop "; /** * 用户信息Http请求头 */ public static final String USER_TOKEN_HEADER = "user"; } 1.3.2 定义消息相关的常量package com.bdysoft.shop.auth.constant; /** * 消息常量 */ public class MessageConstant { public static final String LOGIN_SUCCESS = "登录成功!"; public static final String USERNAME_PASSWORD_ERROR = "用户名或密码错误!"; public static final String CREDENTIALS_EXPIRED = "该账户的登录凭证已过期,请重新登录!"; public static final String ACCOUNT_DISABLED = "该账户已被禁用,请联系管理员!"; public static final String ACCOUNT_LOCKED = "该账号已被锁定,请联系管理员!"; public static final String ACCOUNT_EXPIRED = "该账号已过期,请联系管理员!"; public static final String PERMISSION_DENIED = "没有访问权限,请联系管理员!"; }1.3.3 创建用户信息import java.util.List; /** * 登录用户信息 * * @author lvwei */ @Data @EqualsAndHashCode(callSuper = false) @NoArgsConstructor public class UserDto { private Long id; private String userName; private String realName; private String password; private Integer status; private Integer storeId; private Boolean isSupper; private String clientId; private List<String> roles; }1.3.4 创建登录用户信息package com.bdysoft.shop.auth.dto; import com.bdysoft.shop.common.domain.UserDto; import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.Collection; /** * 登录用户信息 */ @Data public class SecurityUser implements UserDetails { private Long id; private String userName; private String realName; private String password; private Boolean enabled; private Boolean isSupper; private Integer storeId; private String clientId; private Collection<SimpleGrantedAuthority> authorities; public SecurityUser() { } public SecurityUser(UserDto userDto) { this.id = userDto.getId(); this.userName = userDto.getUserName(); this.realName = userDto.getRealName(); this.password = userDto.getPassword(); this.enabled = userDto.getStatus() == 1; this.storeId = userDto.getStoreId(); this.clientId = userDto.getClientId(); this.isSupper = userDto.getIsSuper(); if (!CollectionUtils.isEmpty(userDto.getRoles())) { authorities = new ArrayList<>(); userDto.getRoles().forEach(item -> authorities.add(new SimpleGrantedAuthority(item))); } } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.userName; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return this.enabled; } } 1.3.5 创建用户管理服务类package com.bdysoft.shop.auth.service.impl; import com.bdysoft.shop.auth.constant.AuthConstant; import com.bdysoft.shop.auth.constant.MessageConstant; import com.bdysoft.shop.auth.dto.SecurityUser; import com.bdysoft.shop.auth.feign.AdminUserService; import com.bdysoft.shop.auth.feign.AppUserService; import com.bdysoft.shop.common.domain.UserDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AccountExpiredException; import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import javax.servlet.http.HttpServletRequest; /** * 用户管理服务类 * * @author lvwei */ public class UserDetailServiceImpl implements UserDetailsService { @Autowired private HttpServletRequest request; @Autowired private AdminUserService adminUserService; @Autowired private AppUserService appUserService; /** * 通过用户名加载用户信息 * 在这里我们针对不同的客户端进行处理 * * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { String clientId = request.getParameter("client_id"); String storeId = request.getParameter("store_id"); UserDto userDto; if (AuthConstant.ADMIN_CLIENT_ID.equals(clientId)) { userDto = adminUserService.loadUserByUsername(username, Long.parseLong(storeId)); } else { userDto = appUserService.loadUserByUsername(username, Long.parseLong(storeId)); } if (userDto == null) { throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR); } userDto.setClientId(clientId); userDto.setStoreId(Integer.parseInt(storeId)); SecurityUser securityUser = new SecurityUser(userDto); if (!securityUser.isEnabled()) { throw new DisabledException(MessageConstant.ACCOUNT_DISABLED); } else if (!securityUser.isAccountNonLocked()) { throw new LockedException(MessageConstant.ACCOUNT_LOCKED); } else if (!securityUser.isAccountNonExpired()) { throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED); } else if (!securityUser.isCredentialsNonExpired()) { throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED); } return securityUser; } }1.3.6 往JWT中添加自定义信息/** * JWT内容增强器 * 添加自定义用户信息 * @author lvwei */ @Component public class JwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); Map<String, Object> info = new HashMap<>(3); //把用户ID设置到JWT中 info.put("user_id", securityUser.getId()); info.put("client_id", securityUser.getClientId()); info.put("store_id", securityUser.getStoreId()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; } }1.3.7 添加授权私有化配置package com.bdysoft.shop.auth.properties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "oauth2") public class Oauth2ConfigProperties { private String scopes; private String secret; private Integer accessTokenValiditySeconds; private Integer refreshTokenValiditySeconds; private String[] authorizedGrantTypes; private String jwtPassword; public String getScopes() { return scopes; } public void setScopes(String scopes) { this.scopes = scopes; } public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public Integer getAccessTokenValiditySeconds() { return accessTokenValiditySeconds; } public void setAccessTokenValiditySeconds(Integer accessTokenValiditySeconds) { this.accessTokenValiditySeconds = accessTokenValiditySeconds; } public Integer getRefreshTokenValiditySeconds() { return refreshTokenValiditySeconds; } public void setRefreshTokenValiditySeconds(Integer refreshTokenValiditySeconds) { this.refreshTokenValiditySeconds = refreshTokenValiditySeconds; } public String[] getAuthorizedGrantTypes() { return authorizedGrantTypes; } public void setAuthorizedGrantTypes(String[] authorizedGrantTypes) { this.authorizedGrantTypes = authorizedGrantTypes; } public String getJwtPassword() { return jwtPassword; } public void setJwtPassword(String jwtPassword) { this.jwtPassword = jwtPassword; } } 1.3.8 认证服务器配置使用@EnableAuthorizationServer注解开启package com.bdysoft.shop.auth.config; import com.bdysoft.shop.auth.component.JwtTokenEnhancer; import com.bdysoft.shop.auth.constant.AuthConstant; import com.bdysoft.shop.auth.properties.Oauth2ConfigProperties; import com.bdysoft.shop.auth.service.impl.UserDetailServiceImpl; import lombok.AllArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; import java.security.KeyPair; import java.util.ArrayList; import java.util.List; /** * 认证服务器配置 * * @author lvwei */ @AllArgsConstructor @Configuration @EnableAuthorizationServer public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter { private final PasswordEncoder passwordEncoder; private final UserDetailServiceImpl userDetailsService; private final AuthenticationManager authenticationManager; private final JwtTokenEnhancer jwtTokenEnhancer; @Autowired private Oauth2ConfigProperties oauth2ConfigProperties; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(AuthConstant.ADMIN_CLIENT_ID) .secret(passwordEncoder.encode(oauth2ConfigProperties.getSecret())) .scopes(oauth2ConfigProperties.getScopes()) .authorizedGrantTypes(oauth2ConfigProperties.getAuthorizedGrantTypes()) .accessTokenValiditySeconds(oauth2ConfigProperties.getAccessTokenValiditySeconds()) .refreshTokenValiditySeconds(oauth2ConfigProperties.getRefreshTokenValiditySeconds()) .and() .withClient(AuthConstant.PORTAL_CLIENT_ID) .secret(passwordEncoder.encode(oauth2ConfigProperties.getSecret())) .scopes(oauth2ConfigProperties.getScopes()) .authorizedGrantTypes(oauth2ConfigProperties.getAuthorizedGrantTypes()) .accessTokenValiditySeconds(oauth2ConfigProperties.getAccessTokenValiditySeconds()) .refreshTokenValiditySeconds(oauth2ConfigProperties.getRefreshTokenValiditySeconds()); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> delegates = new ArrayList<>(); delegates.add(jwtTokenEnhancer); delegates.add(accessTokenConverter()); enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器 endpoints.authenticationManager(authenticationManager) .userDetailsService(userDetailsService) //配置加载用户信息的服务 .accessTokenConverter(accessTokenConverter()) .tokenEnhancer(enhancerChain); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setKeyPair(keyPair()); return jwtAccessTokenConverter; } @Bean public KeyPair keyPair() { //从classpath下的证书中获取秘钥对 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), oauth2ConfigProperties.getJwtPassword().toCharArray()); return keyStoreKeyFactory.getKeyPair("jwt", oauth2ConfigProperties.getJwtPassword().toCharArray()); } } 1.3.9 SpringSecurity配置添加SpringSecurity配置,允许认证相关路径的访问及表单登录package com.bdysoft.shop.auth.config; import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * SpringSecurity配置 */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() .antMatchers("/rsa/publicKey").permitAll() .antMatchers("/v2/api-docs").permitAll() .anyRequest().authenticated(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } 1.3.10 自定义Token返回结口package com.bdysoft.shop.auth.dto; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; /** * Oauth2获取Token返回信息封装 * @author lvwei */ @Data @EqualsAndHashCode(callSuper = false) @Builder public class Oauth2TokenDto { private String token; private String refreshToken; private String tokenHead; private int expiresIn; } 1.3.11 获取RSA公钥接口package com.bdysoft.shop.auth.controller; import com.bdysoft.shop.auth.constant.AuthConstant; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.*; import java.security.KeyPair; import java.security.Principal; import java.security.interfaces.RSAPublicKey; import java.util.Map; /** * 获取RSA公钥接口 * @author lvwei */ @RestController= public class KeyPairController { @Autowired private KeyPair keyPair; @GetMapping("/rsa/publicKey") public Map<String, Object> getKey() { RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAKey key = new RSAKey.Builder(publicKey).build(); return new JWKSet(key).toJSONObject(); } }1.3.12 自定义Oauth2获取令牌接口package com.bdysoft.shop.auth.controller; import com.bdysoft.shop.auth.constant.AuthConstant; import com.bdysoft.shop.auth.dto.Oauth2TokenDto; import com.bdysoft.shop.common.result.BaseResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; import java.util.Map; /** * 自定义Oauth2获取令牌接口 */ @RestController @RequestMapping("/oauth") public class AuthController { @Autowired private TokenEndpoint tokenEndpoint; @RequestMapping(value = "/token", method = RequestMethod.POST) public BaseResult postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder() .token(oAuth2AccessToken.getValue()) .refreshToken(oAuth2AccessToken.getRefreshToken().getValue()) .expiresIn(oAuth2AccessToken.getExpiresIn()) .tokenHead(AuthConstant.JWT_TOKEN_PREFIX).build(); return BaseResult.ok().data(oauth2TokenDto); } } 1.3.11 Oauth2ConfigProperties配置package com.bdysoft.shop.auth.properties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "oauth2") public class Oauth2ConfigProperties { private String scopes; private String secret; private Integer accessTokenValiditySeconds; private Integer refreshTokenValiditySeconds; private String[] authorizedGrantTypes; private String jwtPassword; public String getScopes() { return scopes; } public void setScopes(String scopes) { this.scopes = scopes; } public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public Integer getAccessTokenValiditySeconds() { return accessTokenValiditySeconds; } public void setAccessTokenValiditySeconds(Integer accessTokenValiditySeconds) { this.accessTokenValiditySeconds = accessTokenValiditySeconds; } public Integer getRefreshTokenValiditySeconds() { return refreshTokenValiditySeconds; } public void setRefreshTokenValiditySeconds(Integer refreshTokenValiditySeconds) { this.refreshTokenValiditySeconds = refreshTokenValiditySeconds; } public String[] getAuthorizedGrantTypes() { return authorizedGrantTypes; } public void setAuthorizedGrantTypes(String[] authorizedGrantTypes) { this.authorizedGrantTypes = authorizedGrantTypes; } public String getJwtPassword() { return jwtPassword; } public void setJwtPassword(String jwtPassword) { this.jwtPassword = jwtPassword; } } 二、搭建微服务网关我们就可以搭建网关服务了,它将作为Oauth2的资源服务、客户端服务使用,对访问微服务的请求进行统一的校验认证和鉴权操作1.1 引入pom依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>${nimbus-jose-jwt.version}</version> </dependency> <dependency> <groupId>com.bdysoft</groupId> <artifactId>shop-cloud-framework</artifactId> <version>1.0-SNAPSHOT</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </exclusion> <exclusion> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency>1.2 修改yml文件spring: datasource: password: username: url: cloud: nacos: discovery: server-addr: gateway: routes: #配置路由路径 - id: shop-cloud-oauth2-api-route uri: lb://shop-cloud-oauth2-api predicates: - Path=/api/** filters: - StripPrefix=1 - id: shop-cloud-oauth2-auth-route uri: lb://shop-cloud-oauth2-auth predicates: - Path=/auth/** filters: - StripPrefix=1 discovery: locator: enabled: true #开启从注册中心动态创建路由的功能 lower-case-service-id: true #使用小写服务名,默认是大写 security: oauth2: resourceserver: jwt: jwk-set-uri: 'http://localhost:9001/auth/rsa/publicKey' #配置RSA的公钥访问地址 redis: database: 0 port: 6379 host: localhost secure: ignore: urls: #配置白名单路径 - "/actuator/**" - "/auth/oauth/token"1.3 正式编码1.3.1 全局跨域配置@Configuration public class GlobalCorsConfig { @Bean public CorsWebFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedMethod("*"); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }1.3.2 配置网关白名单@Data @EqualsAndHashCode(callSuper = false) @Component @ConfigurationProperties(prefix="secure.ignore") public class IgnoreUrlsConfig { private List<String> urls; }1.3.3 自定义没有权限访问处理器@Component public class RestfulAccessDeniedHandler implements ServerAccessDeniedHandler { @Override public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.OK); response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); response.getHeaders().set("Access-Control-Allow-Origin", "*"); response.getHeaders().set("Cache-Control", "no-cache"); String body = JSONUtil.toJsonStr(BaseResult.error(denied.getMessage())); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8"))); return response.writeWith(Mono.just(buffer)); } }3.4 自定义没有登录或token过期时处理器@Component public class RestAuthenticationEntryPoint implements ServerAuthenticationEntryPoint { @Override public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.OK); response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); response.getHeaders().set("Access-Control-Allow-Origin", "*"); response.getHeaders().set("Cache-Control", "no-cache"); String body = JSONUtil.toJsonStr(BaseResult.error(e.getMessage())); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8"))); return response.writeWith(Mono.just(buffer)); } }1.3.5 鉴权管理器,用于判断是否有资源的访问权限/** * 鉴权管理器,用于判断是否有资源的访问权限 * @author lvwei */ @Component public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private IgnoreUrlsConfig ignoreUrlsConfig; @Override public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) { ServerHttpRequest request = authorizationContext.getExchange().getRequest(); URI uri = request.getURI(); PathMatcher pathMatcher = new AntPathMatcher(); //白名单路径直接放行 List<String> ignoreUrls = ignoreUrlsConfig.getUrls(); for (String ignoreUrl : ignoreUrls) { if (pathMatcher.match(ignoreUrl, uri.getPath())) { return Mono.just(new AuthorizationDecision(true)); } } //对应跨域的预检请求直接放行 if(request.getMethod()== HttpMethod.OPTIONS){ return Mono.just(new AuthorizationDecision(true)); } //不同用户体系登录不允许互相访问 try { String token = request.getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER); if(StrUtil.isEmpty(token)){ return Mono.just(new AuthorizationDecision(false)); } String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, ""); JWSObject jwsObject = JWSObject.parse(realToken); String userStr = jwsObject.getPayload().toString(); UserDto userDto = JSONUtil.toBean(userStr, UserDto.class); if (AuthConstant.ADMIN_CLIENT_ID.equals(userDto.getClientId()) && !pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) { return Mono.just(new AuthorizationDecision(false)); } if (AuthConstant.PORTAL_CLIENT_ID.equals(userDto.getClientId()) && pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) { return Mono.just(new AuthorizationDecision(false)); } } catch (ParseException e) { e.printStackTrace(); return Mono.just(new AuthorizationDecision(false)); } //非管理端路径直接放行 if (!pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) { return Mono.just(new AuthorizationDecision(true)); } //管理端路径需校验权限 Map<Object, Object> resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstant.RESOURCE_ROLES_MAP_KEY); Iterator<Object> iterator = resourceRolesMap.keySet().iterator(); List<String> authorities = new ArrayList<>(); while (iterator.hasNext()) { String pattern = (String) iterator.next(); if (pathMatcher.match(pattern, uri.getPath())) { authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern))); } } authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList()); //认证通过且角色匹配的用户可访问当前路径 return mono .filter(Authentication::isAuthenticated) .flatMapIterable(Authentication::getAuthorities) .map(GrantedAuthority::getAuthority) .any(authorities::contains) .map(AuthorizationDecision::new) .defaultIfEmpty(new AuthorizationDecision(false)); } }1.3.6 网关服务配置安全配置Gateway使用的是WebFlux,所以需要使用@EnableWebFluxSecurity注解开启@AllArgsConstructor @Configuration @EnableWebFluxSecurity public class ResourceServerConfig { private final IgnoreUrlsConfig ignoreUrlsConfig; private final AuthorizationManager authorizationManager; private final RestfulAccessDeniedHandler restfulAccessDeniedHandler; private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.oauth2ResourceServer().jwt() .jwtAuthenticationConverter(jwtAuthenticationConverter()); http.authorizeExchange() .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(), String.class)).permitAll()//白名单配置 .anyExchange().access(authorizationManager)//鉴权管理器配置 .and().exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权 .authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证 .and().csrf().disable(); return http.build(); } @Bean public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX); jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); } }1.3.7 全局过滤器实现一个全局过滤器AuthGlobalFilter,当鉴权通过后将JWT令牌中的用户信息解析出来,然后存入请求的Header中,这样后续服务就不需要解析JWT令牌了,可以直接从请求的Header中获取到用户信息。@Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER); if (StrUtil.isEmpty(token)) { return chain.filter(exchange); } try { //从token中解析用户信息并设置到Header中去 String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, ""); JWSObject jwsObject = JWSObject.parse(realToken); String userStr = jwsObject.getPayload().toString(); LOGGER.info("AuthGlobalFilter.filter() user:{}",userStr); ServerHttpRequest request = exchange.getRequest().mutate().header(AuthConstant.USER_TOKEN_HEADER, userStr).build(); exchange = exchange.mutate().request(request).build(); } catch (ParseException e) { e.printStackTrace(); } return chain.filter(exchange); } @Override public int getOrder() { return 0; } } 1.3.8 白名单路径访问时需要移除JWT请求头/** * 白名单路径访问时需要移除JWT请求头 */ @Component public class IgnoreUrlsRemoveJwtFilter implements WebFilter { @Autowired private IgnoreUrlsConfig ignoreUrlsConfig; @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); URI uri = request.getURI(); PathMatcher pathMatcher = new AntPathMatcher(); //白名单路径移除JWT请求头 List<String> ignoreUrls = ignoreUrlsConfig.getUrls(); for (String ignoreUrl : ignoreUrls) { if (pathMatcher.match(ignoreUrl, uri.getPath())) { request = exchange.getRequest().mutate().header(AuthConstant.JWT_TOKEN_HEADER, "").build(); exchange = exchange.mutate().request(request).build(); return chain.filter(exchange); } } return chain.filter(exchange); } }
2022年03月24日
173 阅读
0 评论
0 点赞
2022-03-12
Spring中的循环依赖
今天我们来聊聊Spring的循环依赖问题,其实这个问题也很好解决,只是人家面试不问你怎么解决,你也不能直接说价格@Lazy就可以了,哎,面试总是这么卷。一:什么是循环依赖?循环依赖,简单的来说,就是对象A依赖了对象B,而对象B又依赖了对象A。在Spring中,一个对象不是new出来就结束了,而是会经历一系列生命周期,就是因为bean的生命周期复杂,才产生了循环依赖问题,大多数场景的循环依赖问题,Spring利用自己的三级缓存机制帮忙解决了,但是还有一些场景需要我们自己来解决。比如:在B对象中,有@Async修饰的方法。当然如果要明白Spring中循环依赖问题,首先得聊聊Spring中Bean生命周期。Spring中Bean的生命周期生命周期比较复杂,面试如果问,就简单的描述下呗。兴许面试官记得还不如你清楚呢,哈哈。Bean的生命周期是描述Spring中Bean的各个阶段,从产生到死亡。Spring管理的对象是Bean。Bean的生成步骤如下:Spring扫描class得到BeanDefinition根据得到的BeanDefinition去生成Bean根据class推断构造方法根据构造方法,通过反射,生成普通对象(原始对象)填充原始对象中的属性(依赖注入)如果原始对象中某个方法被AOP了(比如加了@@Transactional),那么则需要根据原始对象生成一个代理对象把最终生成的代理对象放入单例池(singleObjects),后续可以通过getBean方法从单例池直接拿当然还有比如Aware回调,初始化,初始化前初始化后等等。单单在我们上述描述的步骤中,第四步就包括了构造方法反射生成对象,那么得到一个原始对象,Spring需要给对象中的属性进行依赖注入,这个注入是怎么样的一个过程呢?依赖注入:比如上文中说的A类,A类中存在一个B类的b属性,所以当A生成了一个原始对象之后,就会给b属性复制,此时就会根据b属性的类型和属性名去BeanFactory中取货去B类所对应的单例Bean。如果此时BeanFactory中存在B所对应的Bean,那么可以直接拿来赋值给b属性,但是如果不存在呢?则需要生成一个B对应的Bean,然后赋值给b属性。可是在创建B类的Bean的过程中不得不走Bean的生命周期,此时就会出现循环依赖问题。三级缓存一级缓存:singletonObjects(结构为beanName:Object,缓存的是已经完整经历生命周期的bean对象)二级缓存:earlySingletonObjects(结构为beanName:Object缓存的是早起的bean对象,还没走完完整生命周期的对象)三级缓存:singleFactories(缓存的是ObjectFactory,即对象工厂,用来创建某个对象的)解决循环依赖如何打破上述的循环呢,加个中间人(缓存) A的Bean在创建过程中,在进⾏依赖注⼊之前,先把A的原始Bean放⼊缓存(提早暴露,只要放到缓存 了,其他Bean需要时就可以从缓存中拿了),放⼊缓存后,再进⾏依赖注⼊,此时A的Bean依赖了B的 Bean,如果B的Bean不存在,则需要创建B的Bean,⽽创建B的Bean的过程和A⼀样,也是先创建⼀个B 的原始对象,然后把B的原始对象提早暴露出来放⼊缓存中,然后在对B的原始对象进⾏依赖注⼊A,此时 能从缓存中拿到A的原始对象(虽然是A的原始对象,还不是最终的Bean),B的原始对象依赖注⼊完了之 后,B的⽣命周期结束,那么A的⽣命周期也能结束。 因为整个过程中,都只有⼀个A原始对象,所以对于B⽽⾔,就算在属性注⼊时,注⼊的是A原始对象,也 没有关系,因为A原始对象在后续的⽣命周期中在堆中没有发⽣变化。 从上⾯这个分析过程中可以得出,只需要⼀个缓存就能解决循环依赖了,那么为什么Spring中还需要 singletonFactories呢? 这是难点,基于上⾯的场景想⼀个问题:如果A的原始对象注⼊给B的属性之后,A的原始对象进⾏了AOP 产⽣了⼀个代理对象,此时就会出现,对于A⽽⾔,它的Bean对象其实应该是AOP之后的代理对象,⽽B 的a属性对应的并不是AOP之后的代理对象,这就产⽣了冲突。 那么B依赖的是A的原始对象,不是需要的A的代理对象。那么如何解决这个问题?这个问题可以说没有办法解决。因为在⼀个Bean的⽣命周期最后,Spring提供了BeanPostProcessor可以去对Bean进⾏加⼯,这个加⼯ 不仅仅只是能修改Bean的属性值,也可以替换掉当前Bean。 举个例⼦:@Component public class User { }@Component public class BeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, Str ing beanName) throws BeansException { // 注意这⾥,⽣成了⼀个新的User对象 if (beanName.equals("user")) { System.out.println(bean); User user = new User(); return user; } return bean; } }public class Test { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); User user = context.getBean("user", User.class); System.out.println(user); } }运⾏main⽅法,得到的打印如下:com.bdysoft.service.User@5e025e70 com.bdysoft.service.User@1b0375b3 所以在BeanPostProcessor中可以完全替换掉某个beanName对应的bean对象。⽽BeanPostProcessor的执⾏在Bean的⽣命周期中是处于属性注⼊之后的,循环依赖是发⽣在属性注⼊ 过程中的,所以很有可能导致,注⼊给B对象的A对象和经历过完整⽣命周期之后的A对象,不是⼀个对 象。这就是有问题的。所以在这种情况下的循环依赖,Spring是解决不了的,因为在属性注⼊时,Spring也不知道A对象后续会 经过哪些BeanPostProcessor以及会对A对象做什么处理。Spring到底解决了哪种情况下的循环依赖 虽然上⾯的情况可能发⽣,但是肯定发⽣得很少,我们通常在开发过程中,不会这样去做,但是,某个 beanName对应的最终对象和原始对象不是⼀个对象却会经常出现,这就是AOP。 AOP就是通过⼀个BeanPostProcessor来实现的,这个BeanPostProcessor就是 AnnotationAwareAspectJAutoProxyCreator,它的⽗类是AbstractAutoProxyCreator,⽽在Spring中 AOP利⽤的要么是JDK动态代理,要么CGLib的动态代理,所以如果给⼀个类中的某个⽅法设置了切⾯, 那么这个类最终就需要⽣成⼀个代理对象。 ⼀般过程就是:A类--->⽣成⼀个普通对象-->属性注⼊-->基于切⾯⽣成⼀个代理对象-->把代理对象放 ⼊singletonObjects单例池中。 ⽽AOP可以说是Spring中除开IOC的另外⼀⼤功能,⽽循环依赖⼜是属于IOC范畴的,所以这两⼤功能想 要并存,Spring需要特殊处理。 如何处理的,就是利⽤了第三级缓存singletonFactories。 ⾸先,singletonFactories中存的是某个beanName对应的ObjectFactory,在bean的⽣命周期中,⽣成完原始对象之后,就会构造⼀个ObjectFactory存⼊singletonFactories中。这个ObjectFactory是⼀个函 数式接⼝,所以⽀持Lambda表达式:() -> getEarlyBeanReference(beanName, mbd, bean) 上⾯的Lambda表达式就是⼀个ObjectFactory,执⾏该Lambda表达式就会去执⾏ getEarlyBeanReference⽅法,⽽该⽅法如下:protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { exposedObject = bp.getEarlyBeanReference(exposedObject, beanName); } } return exposedObject; } 该⽅法会去执⾏SmartInstantiationAwareBeanPostProcessor中的getEarlyBeanReference⽅法,⽽这 个接⼝下的实现类中只有两个类实现了这个⽅法,⼀个是AbstractAutoProxyCreator,⼀个是 InstantiationAwareBeanPostProcessorAdapter,它的实现如下:所以很明显,在整个Spring中,默认就只有AbstractAutoProxyCreator真正意义上实现了 getEarlyBeanReference⽅法,⽽该类就是⽤来进⾏AOP的。上⽂提到的 AnnotationAwareAspectJAutoProxyCreator的⽗类就是AbstractAutoProxyCreator。 那么getEarlyBeanReference⽅法到底在⼲什么? ⾸先得到⼀个cachekey,cachekey就是beanName。 然后把beanName和bean(这是原始对象)存⼊earlyProxyReferences中 调⽤wrapIfNecessary进⾏AOP,得到⼀个代理对象。那么,什么时候会调⽤getEarlyBeanReference⽅法呢?回到循环依赖的场景中: 左边⽂字: 这个ObjectFactory就是上⽂说的labmda表达式,中间有getEarlyBeanReference⽅法,注意存⼊ singletonFactories时并不会执⾏lambda表达式,也就是不会执⾏getEarlyBeanReference⽅法 右边⽂字: 从singletonFactories根据beanName得到⼀个ObjectFactory,然后执⾏ObjectFactory,也就是执⾏ getEarlyBeanReference⽅法,此时会得到⼀个A原始对象经过AOP之后的代理对象,然后把该代理对象 放⼊earlySingletonObjects中,注意此时并没有把代理对象放⼊singletonObjects中,那什么时候放⼊ 到singletonObjects中呢? 我们这个时候得来理解⼀下earlySingletonObjects的作⽤,此时,我们只得到了A原始对象的代理对象, 这个对象还不完整,因为A原始对象还没有进⾏属性填充,所以此时不能直接把A的代理对象放⼊ singletonObjects中,所以只能把代理对象放⼊earlySingletonObjects,假设现在有其他对象依赖了A, 那么则可以从earlySingletonObjects中得到A原始对象的代理对象了,并且是A的同⼀个代理对象。 当B创建完了之后,A继续进⾏⽣命周期,⽽A在完成属性注⼊后,会按照它本身的逻辑去进⾏AOP,⽽此 时我们知道A原始对象已经经历过了AOP,所以对于A本身⽽⾔,不会再去进⾏AOP了,那么怎么判断⼀个 对象是否经历过了AOP呢?会利⽤上⽂提到的earlyProxyReferences,在AbstractAutoProxyCreator的 postProcessAfterInitialization⽅法中,会去判断当前beanName是否在earlyProxyReferences,如果 在则表示已经提前进⾏过AOP了,⽆需再次进⾏AOP。 对于A⽽⾔,进⾏了AOP的判断后,以及BeanPostProcessor的执⾏之后,就需要把A对应的对象放⼊ singletonObjects中了,但是我们知道,应该是要A的代理对象放⼊singletonObjects中,所以此时需要 从earlySingletonObjects中得到代理对象,然后⼊singletonObjects中。{dotted startColor="#ff6c6c" endColor="#1989fa"/}⾄此总结⼀下三级缓存:singletonObjects:缓存某个beanName对应的经过了完整⽣命周期的beanearlySingletonObjects:缓存提前拿原始对象进⾏了AOP之后得到的代理对象,原始对象还没有进⾏ 属性注⼊和后续的BeanPostProcessor等⽣命周期singletonFactories:缓存的是⼀个ObjectFactory,主要⽤来去⽣成原始对象进⾏了AOP之后得到的 代理对象,在每个Bean的⽣成过程中,都会提前暴露⼀个⼯⼚,这个⼯⼚可能⽤到,也可能⽤不到, 如果没有出现循环依赖依赖本bean,那么这个⼯⼚⽆⽤,本bean按照⾃⼰的⽣命周期执⾏,执⾏完后 直接把本bean放⼊singletonObjects中即可,如果出现了循环依赖依赖了本bean,则另外那个bean执 ⾏ObjectFactory提交得到⼀个AOP之后的代理对象(如果有AOP的话,如果⽆需AOP,则直接得到⼀ 个原始对象)。其实还要⼀个缓存,就是earlyProxyReferences,它⽤来记录某个原始对象是否进⾏过AOP了。
2022年03月12日
38 阅读
0 评论
0 点赞
2022-03-09
centos 7 防火墙操作
查看防火墙状态firewall-cmd --state停止firewallsystemctl stop firewalld.service禁止firewall开机启动systemctl disable firewalld.service Centos7开放及查看端口– 开放指定端口firewall-cmd --zone=public --add-port=1935/tcp --permanent– 关闭指定端口firewall-cmd --zone=public --remove-port=5672/tcp --permanent– 重启防火墙firewall-cmd --reload
2022年03月09日
74 阅读
0 评论
0 点赞
2022-02-28
费曼学习法
第一步:假装把它(知识、概念)教给一个小孩子。 拿出一张白纸,在上方写下你想要学习的主题。想一下,如果你要把它教给一个孩子,你会讲哪些,并写下来。这里你的教授对象不是你自己那些聪明的成年朋友,而是一个8岁的孩子,他的词汇量和注意力刚好能够理解基本概念和关系。 许多人会倾向于使用复杂的词汇和行话来掩盖他们不明白的东西。问题是我们只在糊弄自己,因为我们不知道自己也不明白。另外,使用行话会隐藏周围人对我们的误解。当你自始至终都用孩子可以理解的简单的语言写出一个想法(提示:只用最常见的单词),那么你便迫使自己在更深层次上理解了该概念,并简化了观点之间的关系和联系。如果你努力,就会清楚地知道自己在哪里还有不明白的地方。这种紧张状态很好——预示着学习的机会到来了。 第二步:回顾。 在第一步中,你不可避免地会卡壳,忘记重要的点,不能解释,或者说不能将重要的概念联系起来。 这一反馈相当宝贵,因为你已经发现了自己知识的边缘。懂得自己能力的界限也是一种能力,你刚刚就确定了一个! 这是学习开始的地方。现在你知道自己在哪里卡住了,那么就回到原始材料,重新学习,直到你可以用基本的术语解释这一概念。认定自己知识的界限,会限制你可能犯的错误,并且在应用该知识时,可以增加成功的几率。 第三步:将语言条理化,简化。 现在你手上有一套自己手写笔记,检查一下确保自己没有从原材料中借用任何行话。将这些笔记用简单的语言组织成一个流畅的故事。 将这个故事大声读出来,如果这些解释不够简单,或者听起来比较混乱,很好,这意味着你想要理解该领域,还需要做一些工作。 第四步(可选):传授 如果你真的想确保你的理解没什么问题,就把它教给另一个人(理想状态下,这个人应该对这个话题知之甚少,或者就找个 8 岁的孩子)。检测知识最终的途径是你能有能力把它传播给另一个人。 这不仅是学习的妙方,还是窥探不同思维方式的窗口,它让你将想法撕开揉碎,从头重组。这种学习方法会让你对观点和概念有更为深入的理解。重要的是,以这种方式解决问题,你可以在别人不知道他们自己在说什么的情况下,理解这个问题。
2022年02月28日
52 阅读
0 评论
0 点赞
2022-02-24
SpringBoot2 + Redis + MySQL实现一个抢红包系统
常见的红包系统,由用户指定金额、红包总数来完成红包的创建,然后通过某个入口将红包下发至目标用户,用户看到红包后,点击红包,随机获取红包,最后,用户可以查看自己抢到的红包。整个业务流程不复杂,难点在于抢红包这个行为可能有很高的并发。所以,系统设计的优化点主要关注在抢红包这个行为上。发红包:用户设置红包总金额、总数量抢红包:用户从总红包中随机获得一定金额没什么好说的,相信大家的微信红包没少抢,一想都明白。看起来业务很简单,却其实还有点小麻烦。首先,抢红包必须保证高可用,不然用户会很愤怒。其次,必须保证系统数据一致性不能超发,不然抢到红包的用户收不到钱,用户会很愤怒。最后一点,系统可能会有很高的并发。OK,分析完直接进行详细设计。所以简简单单只有两个接口:发红包、抢红包。 红包活动表CREATE TABLE `t_redpack_activity` ( `id` bigint(20) NOT NULL COMMENT '主键', `total_amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '总金额', `surplus_amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '剩余金额', `total` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包总数', `surplus_total` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包剩余总数', `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户编号', `version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;红包表CREATE TABLE `t_redpack` ( `id` bigint(20) NOT NULL COMMENT '主键', `activity_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '红包活动ID', `amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '金额', `status` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '红包状态 1可用 2不可用', `version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;明细表CREATE TABLE `t_redpack_detail` ( `id` bigint(20) NOT NULL COMMENT '主键', `amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '金额', `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户编号', `redpack_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包编号', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;活动表,就是你发了多少个红包,并且需要维护剩余金额。明细表是用户抢到的红包明细。红包表是每一个具体的红包信息。为什么需要三个表呢?事实上如果没有红包表也是可以的。但我们的方案预先分配红包需要使用一张表来记录红包的信息,所以设计的时候才有此表。OK,分析完表结构其实方案已经七七八八差不多了。请接着看下面的方案,从简单到复杂的过度。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="基于分布式锁的实现"/}基于分布式锁的实现最为简单粗暴,整个抢红包接口以activityId作为key进行加锁,保证同一批红包抢行为都是串行执行。分布式锁的实现是由spring-integration-redis工程提供,核心类是RedisLockRegistry。锁通过Redis的lua脚本实现,且实现了阻塞式本地可重入。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="基于乐观锁的实现"/}第二种方式,为红包活动表增加乐观锁版本控制,当多个线程同时更新同一活动表时,只有一个clien会成功。其它失败的client进行循环重试,设置一个最大循环次数即可。此种方案可以实现并发情况下的处理,但是冲突很大。因为每次只有一个人会成功,其他client需要进行重试,即使重试也只能保证一次只有一个人成功,因此TPS很低。当设置的失败重试次数小于发放的红包数时,可能导致最后有人没抢到红包,实际上还有剩余红包{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="基于悲观锁的实现"/}由于红包活动表增加乐观锁冲突很大,所以可以考虑使用使用悲观锁:select * from t_redpack_activity where id = #{id} for update,注意悲观锁必须在事务中才能使用。此时,所有的抢红包行为变成了串行。此种情况下,悲观锁的效率远大于乐观锁。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="预先分配红包,基于乐观锁的实现"/}可以看到,如果我们将乐观锁的维度加在红包明细上,那么冲突又会降低。因为之前红包明细是用户抢到后才创建的,那么现在需要预先分配红包,即创建红包活动时即生成N个红包,通过状态来控制可用/不可用。这样,当多个client抢红包时,获取该活动下所有可用的红包明细,随机返回其中一条然后再去更新,更新成功则代表用户抢到了该红包,失败则代表出现了冲突,可以循环进行重试。如此,冲突便被降低了。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="基于Redis队列的实现"/}和上一个方案类似,不过,用户发放红包时会创建相应数量的红包,并且加入到Redis队列中。抢红包时会将其弹出。Redis队列很好的契合了我们的需求,每次弹出都不会出现重复的元素,用完即销毁。缺陷:抢红包时一旦从队列弹出,此时系统崩溃,恢复后此队列中的红包明细信息已丢失,需要人工补偿。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="基于Redis队列,异步入库"/} 这种方案的是抢到红包后不操作数据库,而是保存持久化信息到Redis中,然后返回成功。通过另外一个线程UserRedpackPersistConsumer,拉取持久化信息进行入库。需要注意的是,此时的拉取动作如果使用普通的pop仍然会出现crash point的问题,所以考虑到可用性,此处使用Redis的BRPOPLPUSH操作,弹出元素后加入备份到另外一个队列,保证此处崩溃后可以通过备份队列自动恢复。崩溃恢复线程CrashRecoveryThread通过定时拉取备份信息,去DB中查证是否持久化成功,如果成功则清除此元素,否则进行补偿并清除此元素。如果在操作数据库的过程中出现异常会记录错误日志redpack.persist.log,此日志使用单独的文件和格式,方便进行补偿(一般不会触发)。
2022年02月24日
55 阅读
0 评论
0 点赞
2022-02-24
浅谈 MyBatis 缓存
1、一级缓存 MyBatis默认开启了一级缓存,一级缓存是在SqlSession层面进行缓存的。即同一个 SqlSession ,多次调用同一个 Mapper 和同一个方法的同一个参数,只会进行一次数据库查询,然后把数据缓存到缓冲中,以后直接先从缓存中取出数据,不会直接去查数据库。但是不同的 SqlSession 对象,因为不用的 SqlSession 都是相互隔离的,所以相同的 Mapper、参数和方法,它还是会再次发送到 SQL 到数据库去执行,返回结果。2、二级缓存为了解决这个问题,需要手动开启二级缓存,在 SqlSessionFactory 层面给各个 SqlSession 对象共享。默认二级缓存是不开启的,需要手动进行配置。在 MyBatis-config.xml 文件中添加<setting name="cacheEnabled" value="true"/>在 xxxMapper.xml 文件中添加<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"/>属性释义: eviction: 缓存的回收策略,默认的是 LRU。LRU - 最近最少使用,移除最长时间不被使用的对象。FIFO - 先进先出,按对象进入缓存的顺序来移除它们。SOFT - 软引用,移除基于垃圾回收器状态和软引用规则的对象。WEAK - 弱引用,更积极地移除基于垃圾收集器和弱引用规则的对象。flushInterval: 缓存刷新间隔。缓存多长时间清空一次,默认不清空,设置一个毫秒值。 readOnly: 是否只读。true(只读):MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户 。不安全,速度快。false(读写,默认):MyBatis 觉得获取的数据可能会被修改,MyBatis 会利用序列化或反序列化的技术克隆一份新的数据。安全,速度相对慢。size: 缓存存放多少个元素。type: 指定自定义缓存的全类名(实现 Cache 接口即可)。PS:要使用二级缓存,对应的 POJO 必须实现序列化接口 。useCache=“true” 是否使用一级缓存,默认 true。sqlSession.clearCache();只是清除当前 session 中的一级缓存。useCache 配置: 如果一条句每次都需要最新的数据,就意味着每次都需要从数据库中查询数据,可以把这个属性设置为 false,如:<select id="selectUserById" resultMap="map" useCache="false">刷新缓存(就是清空缓存)二级缓存默认会在 insert、update、delete 操作后刷新缓存。可以手动配置不更新缓存<update id="updateUserById" parameterType="User" flushCache="false">3、自定义缓存自定义缓存对象,该对象必须实现 org.apache.ibatis.cache.Cache 接口。为了方便日后的开发工作和降低学习成本,我们可以使用第三方缓存,推荐使用 EhCache。EhCache 是一个快速的、轻量级的、易于使用的、进程内的缓存。它支持 read-only 和 read/write 缓存,内存和磁盘缓存。是一个非常轻量级的缓存实现,而且从 1.2 之后就支持了集群,目前的最新版本是 2.8。部署 3.1. 导入所需 jar: 核心包:ehcache-core-2.6.8.jar整合包:MyBatis-ehcache-1.0.3.jar依赖 jar 包:slf4j-api-1.6.6.jar,slf4j-jdk14-1.6.6.jar3.2. 导入所需配置文件: ehcache.xml,下面是对配置文件的一些讲解:<?xml version="1.0" encoding="UTF-8"?><ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd"><defaultCache maxElementsInMemory="10000" maxElementsOnDisk="10000000" eternal="false" overflowToDisk="true" timeToIdleSeconds="120" timeToLiveSeconds="120" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> <!-- 属性说明:diskStore:指定数据在磁盘中的存储位置。defaultCache:当借助 CacheManager.add("demoCache") 创建 Cache 时,EhCache 便会采用 <defalutCache/> 指定的的管理策略。以下属性是必须的:maxElementsInMemory - 在内存中缓存的 element 的最大数目。maxElementsOnDisk - 在磁盘上缓存的 element 的最大数目,若是 0 表示无穷大。eternal - 设定缓存的 elements 是否永远不过期。如果为 true,则缓存的数据始终有效,如果为 false 那么还要根据 timeToIdleSeconds,timeToLiveSeconds 判断。overflowToDisk - 设定当内存缓存溢出的时候是否将过期的 element 缓存到磁盘上。以下属性是可选的:timeToIdleSeconds - 当缓存在 EhCache 中的数据前后两次访问的时间超过 timeToIdleSeconds 的属性取值时,这些数据便会删除,默认值是 0,也就是可闲置时间无穷大。timeToLiveSeconds - 缓存 element 的有效生命期,默认是 0,也就是 element 存活时间无穷大。diskSpoolBufferSizeMB - 这个参数设置 DiskStore (磁盘缓存)的缓存区大小.默认是 30MB.每个 Cache 都应该有自己的一个缓冲区。diskPersistent - 在 VM 重启的时候是否启用磁盘保存 EhCache 中的数据,默认是 false。diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是 120 秒。每个 120s,相应的线程会进行一次 EhCache 中数据的清理工作。memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的 element 加入的时候, 移除缓存中 element 的策略。默认是 LRU(最近最少使用),可选的有 LFU(最不常使用)和 FIFO(先进先出)。 -->3.3 在 xxxMapper.xml 文件中进行配置<cache type="org.MyBatis.caches.ehcache.EhcacheCache"/>如果在其他的 xxxMapper.xml 文件中也想要使用,只需在该文件下配置,namespace 为配置过得命名空间地址。<cache-ref namespace="mapper.UserMapper"/>MyBatis 默认是启用 cache 的,所以对于某一条不想被 cache 的 sql 需要把 useCache=“false” 加上。<select id="selectUserById" useCache="false">进行测试后,会在 磁盘保存路径 中产生了相关的文件。
2022年02月24日
45 阅读
0 评论
0 点赞
2022-02-22
Spring Cloud Alibaba:Sentinel实现熔断与限流
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案,Sentinel 作为其核心组件之一,具有熔断与限流等一系列服务保护功能,本文将对其用法进行详细介绍。Sentinel简介随着微服务的流行,服务和服务之间的稳定性变得越来越重要。 Sentinel 以流量为切入点,从 流量控制 、 熔断降级 、 系统负载保护 等多个维度保护服务的稳定性。Sentinel具有如下特性:丰富的应用场景:承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀,可以实时熔断下游不可用应用;完备的实时监控:同时提供实时的监控功能。可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况;广泛的开源生态:提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合;完善的 SPI 扩展点:提供简单易用、完善的 SPI 扩展点。您可以通过实现扩展点,快速的定制逻辑。创建sentinel-service模块这里我们创建一个sentinel-service模块,用于演示Sentinel的熔断与限流功能。在pom.xml中添加相关依赖,这里我们使用Nacos作为注册中心,所以需要同时添加Nacos的依赖:<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>在application.yml中添加相关配置,主要是配置了Nacos和Sentinel控制台的地址server: port: 8401 spring: application: name: sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 #配置Nacos地址 sentinel: transport: dashboard: localhost:8080 #配置sentinel dashboard地址 port: 8719 service-url: user-service: http://nacos-user-service management: endpoints: web: exposure: include: '*'限流功能Sentinel Starter 默认为所有的 HTTP 服务提供了限流埋点,我们也可以通过使用 @SentinelResource 来自定义一些限流行为。 创建RateLimitController类 用于测试熔断和限流功能。@RestController @RequestMapping("/rateLimit") public class RateLimitController { /** * 按资源名称限流,需要指定限流处理逻辑 */ @GetMapping("/byResource") @SentinelResource(value = "byResource",blockHandler = "handleException") public CommonResult byResource() { return new CommonResult("按资源名称限流", 200); } /** * 按URL限流,有默认的限流处理逻辑 */ @GetMapping("/byUrl") @SentinelResource(value = "byUrl",blockHandler = "handleException") public CommonResult byUrl() { return new CommonResult("按url限流", 200); } public CommonResult handleException(BlockException exception){ return new CommonResult(exception.getClass().getCanonicalName(),200); } }根据资源名称限流我们可以根据@SentinelResource注解中定义的value(资源名称)来进行限流操作,但是需要指定限流处理逻辑。流控规则可以在Sentinel控制台进行配置,由于我们使用了Nacos注册中心,我们先启动Nacos和sentinel-service;由于Sentinel采用的懒加载规则,需要我们先访问下接口,Sentinel控制台中才会有对应服务信息,我们先访问下该接口:http://localhost:8401/rateLimit/byResource在Sentinel控制台配置流控规则,根据@SentinelResource注解的value值:快速访问上面的接口,可以发现返回了自己定义的限流处理信息:根据URL限流我们还可以通过访问的URL来限流,会返回默认的限流处理信息。在Sentinel控制台配置流控规则,使用访问的URL:多次访问该接口,会返回默认的限流处理结果:http://localhost:8401/rateLimit/byUrl自定义限流处理逻辑我们可以自定义通用的限流处理逻辑,然后在@SentinelResource中指定。创建CustomBlockHandler类用于自定义限流处理逻辑:public class CustomBlockHandler { public CommonResult handleException(BlockException exception){ return new CommonResult("自定义限流信息",200); } }在RateLimitController中使用自定义限流处理逻辑:@RestController @RequestMapping("/rateLimit") public class RateLimitController { /** * 自定义通用的限流处理逻辑 */ @GetMapping("/customBlockHandler") @SentinelResource(value = "customBlockHandler", blockHandler = "handleException",blockHandlerClass = CustomBlockHandler.class) public CommonResult blockHandler() { return new CommonResult("限流成功", 200); } }熔断功能Sentinel 支持对服务间调用进行保护,对故障应用进行熔断操作,这里我们使用RestTemplate来调用nacos-user-service服务所提供的接口来演示下该功能。首先我们需要使用@SentinelRestTemplate来包装下RestTemplate实例:@Configuration public class RibbonConfig { @Bean @SentinelRestTemplate public RestTemplate restTemplate(){ return new RestTemplate(); } }添加CircleBreakerController类,定义对nacos-user-service提供接口的调用:@RestController @RequestMapping("/breaker") public class CircleBreakerController { private Logger LOGGER = LoggerFactory.getLogger(CircleBreakerController.class); @Autowired private RestTemplate restTemplate; @Value("${service-url.user-service}") private String userServiceUrl; @RequestMapping("/fallback/{id}") @SentinelResource(value = "fallback",fallback = "handleFallback") public CommonResult fallback(@PathVariable Long id) { return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id); } @RequestMapping("/fallbackException/{id}") @SentinelResource(value = "fallbackException",fallback = "handleFallback2", exceptionsToIgnore = {NullPointerException.class}) public CommonResult fallbackException(@PathVariable Long id) { if (id == 1) { throw new IndexOutOfBoundsException(); } else if (id == 2) { throw new NullPointerException(); } return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id); } public CommonResult handleFallback(Long id) { User defaultUser = new User(-1L, "defaultUser", "123456"); return new CommonResult<>(defaultUser,"服务降级返回",200); } public CommonResult handleFallback2(@PathVariable Long id, Throwable e) { LOGGER.error("handleFallback2 id:{},throwable class:{}", id, e.getClass()); User defaultUser = new User(-2L, "defaultUser2", "123456"); return new CommonResult<>(defaultUser,"服务降级返回",200); } }启动nacos-user-service和sentinel-service服务:由于我们并没有在nacos-user-service中定义id为4的用户,所有访问如下接口会返回服务降级结果:http://localhost:8401/breaker/fallback/4{ "data": { "id": -1, "username": "defaultUser", "password": "123456" }, "message": "服务降级返回", "code": 200 }由于我们使用了exceptionsToIgnore参数忽略了NullPointerException,所以我们访问接口报空指针时不会发生服务降级:http://localhost:8401/breaker/fallbackException/2与Feign结合使用Sentinel也适配了Feign组件,我们使用Feign来进行服务间调用时,也可以使用它来进行熔断。首先我们需要在pom.xml中添加Feign相关依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>在application.yml中打开Sentinel对Feign的支持:feign: sentinel: enabled: true #打开sentinel对feign的支持在应用启动类上添加@EnableFeignClients启动Feign的功能;创建一个UserService接口,用于定义对nacos-user-service服务的调用:@FeignClient(value = "nacos-user-service",fallback = UserFallbackService.class) public interface UserService { @PostMapping("/user/create") CommonResult create(@RequestBody User user); @GetMapping("/user/{id}") CommonResult<User> getUser(@PathVariable Long id); @GetMapping("/user/getByUsername") CommonResult<User> getByUsername(@RequestParam String username); @PostMapping("/user/update") CommonResult update(@RequestBody User user); @PostMapping("/user/delete/{id}") CommonResult delete(@PathVariable Long id); }创建UserFallbackService类实现UserService接口,用于处理服务降级逻辑:@Component public class UserFallbackService implements UserService { @Override public CommonResult create(User user) { User defaultUser = new User(-1L, "defaultUser", "123456"); return new CommonResult<>(defaultUser,"服务降级返回",200); } @Override public CommonResult<User> getUser(Long id) { User defaultUser = new User(-1L, "defaultUser", "123456"); return new CommonResult<>(defaultUser,"服务降级返回",200); } @Override public CommonResult<User> getByUsername(String username) { User defaultUser = new User(-1L, "defaultUser", "123456"); return new CommonResult<>(defaultUser,"服务降级返回",200); } @Override public CommonResult update(User user) { return new CommonResult("调用失败,服务被降级",500); } @Override public CommonResult delete(Long id) { return new CommonResult("调用失败,服务被降级",500); } }在UserFeignController中使用UserService通过Feign调用nacos-user-service服务中的接口:@RestController @RequestMapping("/user") public class UserFeignController { @Autowired private UserService userService; @GetMapping("/{id}") public CommonResult getUser(@PathVariable Long id) { return userService.getUser(id); } @GetMapping("/getByUsername") public CommonResult getByUsername(@RequestParam String username) { return userService.getByUsername(username); } @PostMapping("/create") public CommonResult create(@RequestBody User user) { return userService.create(user); } @PostMapping("/update") public CommonResult update(@RequestBody User user) { return userService.update(user); } @PostMapping("/delete/{id}") public CommonResult delete(@PathVariable Long id) { return userService.delete(id); } }调用如下接口会发生服务降级,返回服务降级处理信息:http://localhost:8401/user/4{ "data": { "id": -1, "username": "defaultUser", "password": "123456" }, "message": "服务降级返回", "code": 200 }使用Nacos存储规则默认情况下,当我们在Sentinel控制台中配置规则时,控制台推送规则方式是通过API将规则推送至客户端并直接更新到内存中。一旦我们重启应用,规则将消失。下面我们介绍下如何将配置规则进行持久化,以存储到Nacos为例。 原理示意图 首先我们直接在配置中心创建规则,配置中心将规则推送到客户端;Sentinel控制台也从配置中心去获取配置信息。 功能演示先在pom.xml中添加相关依赖:<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>修改application.yml配置文件,添加Nacos数据源配置:spring: cloud: sentinel: datasource: ds1: nacos: server-addr: localhost:8848 dataId: ${spring.application.name}-sentinel groupId: DEFAULT_GROUP data-type: json rule-type: flow在Nacos中添加配置:添加配置信息如下:[ { "resource": "/rateLimit/byUrl", "limitApp": "default", "grade": 1, "count": 1, "strategy": 0, "controlBehavior": 0, "clusterMode": false } ]相关参数解释:resource:资源名称;limitApp:来源应用;grade:阈值类型,0表示线程数,1表示QPS;count:单机阈值;strategy:流控模式,0表示直接,1表示关联,2表示链路;controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;clusterMode:是否集群。发现Sentinel控制台已经有了如下限流规则:快速访问测试接口,可以发现返回了限流处理信息:使用到的模块springcloud-learning├── nacos-user-service -- 注册到nacos的提供User对象CRUD接口的服务└── sentinel-service -- sentinel功能测试服务
2022年02月22日
41 阅读
0 评论
0 点赞
2022-02-21
详解:ConcurrentHashMap底层结构+实现原理
前言 HashMap是一个非常优秀的类,使用也非常频繁。唯一的遗憾就是HashMap不是线程安全的。理解了HashMap,再来看ConcurrentHashMap会有事半功倍的效果,因为ConcurrentHashMap底层数据结构、核心方法几乎和HashMap一模一样,只是在多线程环境下做了很多保证线程安全的操作。JDK早期提供了线程安全的HashMap类,那就是Hashtable,底层几乎把所有的方法都加上了锁,导致效率太低。JDK1.5开始,JUC包中提供了一个更高效的、线程安全的HashMap类,那就是ConcurrentHashMap。ConcurrentHashMap可以看到ConcurrentHashMap主要实现了Map和Serializable接口。 内部结构 想要读懂容器类的源码,必须先了解它的数据结构。所以先看看ConcurrentHashMap的内部结构,重点关注以下几个中的属性即可:public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { private static final long serialVersionUID = 7249069246763182397L; // 最大容量 private static final int MAXIMUM_CAPACITY = 1 << 30; // 默认容量 private static final int DEFAULT_CAPACITY = 16; // 最大可能的数组大小 static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 默认的并发级别(不使用,为了兼容之前的版本) private static final int DEFAULT_CONCURRENCY_LEVEL = 16; // 默认加载因子 private static final float LOAD_FACTOR = 0.75f; // 链表转红黑树阈值 static final int TREEIFY_THRESHOLD = 8; // 红黑树退化成链表的阈值 static final int UNTREEIFY_THRESHOLD = 6; // (红黑)树化时,table数组最小值 // 至少是4倍的TREEIFY_THRESHOLD static final int MIN_TREEIFY_CAPACITY = 64; // 第一次新增元素时初始化,始终是2的幂 transient volatile Node<K,V>[] table; // 扩容时用,代表扩容后的数组 private transient volatile Node<K,V>[] nextTable; // 节点hash的特殊值 static final int MOVED = -1; // 转移节点的hash值 static final int TREEBIN = -2; // (红黑)树根节点的hash值 static final int RESERVED = -3; // 临时保留的hash值 static final int HASH_BITS = 0x7fffffff; // 普通节点hash的可用位 // 控制table初始化和扩容的字段 // -1 初始化中 // -n 表示n-1个线程正在扩容中 // 0 使用默认容量进行初始化 // >0 使用多少容量 private transient volatile int sizeCtl; }构造方法 ConcurrentHashMap提供了5个构造方法,主要关注3个public ConcurrentHashMap(int initialCapacity) { if (initialCapacity < 0) // 传入的初始化容量不能小于0 throw new IllegalArgumentException(); // 根据传入的capacity计算合理的capacity int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); this.sizeCtl = cap; } public ConcurrentHashMap(int initialCapacity, float loadFactor) { // concurrencyLevel传入了1 this(initialCapacity, loadFactor, 1); } public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) // loadFactor、initialCapacity、concurrencyLevel都不能小于0 throw new IllegalArgumentException(); if (initialCapacity < concurrencyLevel) // Use at least as many bins initialCapacity = concurrencyLevel; // as estimated threads long size = (long)(1.0 + (long)initialCapacity / loadFactor); // 根据传入的capacity和loadFactor计算合理的capacity int cap = (size >= (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size); this.sizeCtl = cap; }构造方法基本只做了参数校验,计算合理的capacity值,并没有初始化数组table核心方法 对于ConcurrentHashMap而言,核心方法毫无疑问就是put和get。所以先来看看put方法的整体逻辑。 put方法 put方法用于往map中添加一个键值对K、V。方法实现如下:public V put(K key, V value) { // 调用自身putVal()方法 // 第三个参数传false,表示map中有相同的key时(equals相等),直接覆盖其value值 return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { // key、value都不能为null if (key == null || value == null) throw new NullPointerException(); // 计算hash值 int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { // 自旋 Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) // 数组尚未初始化,进行初始化 tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 当前槽为null(没有数据) if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) // CAS的方法把k、v包装成Node节点,放在这个槽上 // 成功后就结束自旋,无需加锁 // 不成功继续自旋 break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) // 当前槽上的节点正在转移(扩容) tab = helpTransfer(tab, f); else { // 当前槽上有值,并且不处于转移状态 V oldVal = null; synchronized (f) { // 锁住当前槽 // 因为只锁住了一个槽(链表头节点、红黑树根节点),也就是数组的一项,所以比JDK1.7中锁住一段(分段锁)的效率更高 if (tabAt(tab, i) == f) { // 当前槽上的节点没有被修改过,double-check if (fh >= 0) { // 该槽上是单链表 binCount = 1; for (Node<K,V> e = f;; ++binCount) { // 遍历当前槽 K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 单链表上找到了相同的key,覆盖其value值 oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { // 【尾插法】把新增节点插入链表,退出自旋 pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { // 当前槽上存的是红黑树,按照红黑树的方式插入元素 Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) // 链表需要转换成红黑树 treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 检检查是否需要扩容,如果需要就扩容 addCount(1L, binCount); return null; }整个put方法大致分为以下几步:1、校验K、V,并计算has值2、进入自旋,判断table是否已经初始化,如果否,则进行初始化;如果是,执行33、判断当前槽上是否为null,如果是,通过CAS的方式新增节点;如果否,执行44、判断当前槽上的节点是否正在转移(扩容过程),如果是,辅助扩容;如果否,执行55、锁住当前槽,如果当前槽上是单链表,按照单链表的方式新增节点,如果是红黑树,按照红黑树的方式新增节点6、判断链表是否需要转换成红黑树,如果是,转换成红黑树7、新增节点完成后,检测是否需要扩容,如果需要,就扩容从源码来看,ConcurrentHashMap中的put方法和HashMap的put方法执行的逻辑相差无几。只是利用了自旋 + CAS、Synchronized等来保证线程安全。接下来深入put方法中使用的一些内部方法:initTable、addCount等 initTable initTable用于初始化table数组,其实现如下:private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { // 自旋 // 外层putVal方法已经判断过这个条件,double-check if ((sc = sizeCtl) < 0) // 有其他的线程正在初始table数组 Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // CAS的方式抢到锁 try { if ((tab = table) == null || tab.length == 0) { // 再次double-check // 执行初始 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }初始化table数组的核心逻辑只有一行new操作,但是为了保证线程安全和高效,采用了double-check + 自旋 + CAS的方式,这也是多线程并发编程的常见手段。addCount addCount方法用来检测是否需要扩容,如果需要就扩容。private final void addCount(long x, int check) { CounterCell[] as; long b, s; if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[ThreadLocalRandom.getProbe() & m]) == null || !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { fullAddCount(x, uncontended); return; } if (check <= 1) return; s = sumCount(); } if (check >= 0) { Node<K,V>[] tab, nt; int n, sc; while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { int rs = resizeStamp(n); if (sc < 0) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) // 有别的线程正在扩容 transfer(tab, nt); } else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) // 没有别的线程正在扩容 transfer(tab, null); s = sumCount(); } } }对于这个方法而言,就是判断要不要扩容,而真正的扩容方法是transfer,所以具体看下transfer方法的实现逻辑/** * tab表示扩容前的数组 * nextTab表示扩容后的新数组(如果为null,表示并没有别的线程在扩容) */ private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { int n = tab.length, stride; if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // subdivide range if (nextTab == null) { // initiating try { // 初始化nextTab,大小为原数组的2倍 @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; nextTab = nt; } catch (Throwable ex) { // try to cope with OOME sizeCtl = Integer.MAX_VALUE; return; } nextTable = nextTab; transferIndex = n; } // 获取新数组的长度 int nextn = nextTab.length; // 如果元素组槽上是转移节点,表示该槽上的节点正在转移 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); boolean advance = true; boolean finishing = false; // to ensure sweep before committing nextTab for (int i = 0, bound = 0;;) { // 自旋 Node<K,V> f; int fh; while (advance) { int nextIndex, nextBound; if (--i >= bound || finishing) advance = false; else if ((nextIndex = transferIndex) <= 0) { // 拷贝已经完成 i = -1; advance = false; } else if (U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { bound = nextBound; i = nextIndex - 1; advance = false; } } if (i < 0 || i >= n || i + n >= nextn) { // 拷贝结束 int sc; if (finishing) { nextTable = null; table = nextTab; sizeCtl = (n << 1) - (n >>> 1); return; } if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return; finishing = advance = true; i = n; // recheck before commit } } else if ((f = tabAt(tab, i)) == null) advance = casTabAt(tab, i, null, fwd); else if ((fh = f.hash) == MOVED) advance = true; // already processed else { synchronized (f) { // 加锁,进行节点拷贝 if (tabAt(tab, i) == f) { // 低位链表和高位链表(前文HashMap中讲过) Node<K,V> ln, hn; if (fh >= 0) { int runBit = fh & n; Node<K,V> lastRun = f; for (Node<K,V> p = f.next; p != null; p = p.next) { int b = p.hash & n; if (b != runBit) { runBit = b; lastRun = p; } } if (runBit == 0) { ln = lastRun; hn = null; } else { hn = lastRun; ln = null; } for (Node<K,V> p = f; p != lastRun; p = p.next) { // 循环拷贝链表节点 // 原数组中的链表会被分成高、低位两个链表,放在新数组中不同的槽 int ph = p.hash; K pk = p.key; V pv = p.val; if ((ph & n) == 0) ln = new Node<K,V>(ph, pk, pv, ln); else hn = new Node<K,V>(ph, pk, pv, hn); } // 链表设置到新数组 setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); // 旧数组上设置转移节点 // 其他线程发下槽上是转移节点后就会等待 setTabAt(tab, i, fwd); advance = true; } else if (f instanceof TreeBin) { // 拷贝红黑树的节点 TreeBin<K,V> t = (TreeBin<K,V>)f; TreeNode<K,V> lo = null, loTail = null; TreeNode<K,V> hi = null, hiTail = null; int lc = 0, hc = 0; for (Node<K,V> e = t.first; e != null; e = e.next) { int h = e.hash; TreeNode<K,V> p = new TreeNode<K,V> (h, e.key, e.val, null, null); if ((h & n) == 0) { if ((p.prev = loTail) == null) lo = p; else loTail.next = p; loTail = p; ++lc; } else { if ((p.prev = hiTail) == null) hi = p; else hiTail.next = p; hiTail = p; ++hc; } } ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin<K,V>(lo) : t; hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new TreeBin<K,V>(hi) : t; setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true; } } } } } }可以看到ConcurrentHashMap中的扩容也是采用了自旋 + CAS + Synchronized来保证线程安全的,除此之外,还添加了转移节点,表示该槽上的节点正在被转移,此时别的线程不要往这个槽写数据。 get方法 get方法用于从map中根据key取value。其实现相比于put方法要简单得多public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; // 根据key计算hash值 int h = spread(key.hashCode()); if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { // table不为空,且当前槽上有数据 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) // 槽上第一个节点就是要取的节点,直接返回value return e.val; } else if (eh < 0) // 槽上的第一个节点红黑树的根节点或者转移节点,调用其find方法查找 return (p = e.find(h, key)) != null ? p.val : null; while ((e = e.next) != null) { // 槽上是单链表,遍历单查找 if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) // 找到需要的节点,返回value return e.val; } } // table没有初始 // 当前槽上没有数据 // 红黑树/转移节点/单链表上未找到 return null; }size方法 size方法用于返回map的节点个数,在HashMap中非常简单,因为定义了一个变量来维护size,但是ConcurrentHashMap并没有定义这样的变量,先来看下其size方法的实现public int size() { // 调用内部sumCount方法 long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); } final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }可以看到最后返回size的值就是baseCount的值 + counterCells数组中的所有值之和。counterCells数组中存的实际上就是table数组中每个槽上的节点个数。baseCount相当于counterCells的优化,在没有竞争的时候使用。实际上也就是分段求和,再汇总的思想。看到方法内部并没有加锁,说明size方法返回的并不是一个准确值,而是一个近似值,因为在汇总的过程中,有可能map中新增或者删除了元素。与JDK1.7的区别一张图就可以直观的感受到ConcurrentHashMap在JDK1.7和JDK1.8的区别对JDK1.7中的ConcurrentHashMap而言内部主要是一个Segment数组,而数组的每一项又是一个HashEntry数组,元素都存在HashEntry数组里。因为每次锁定的是Segment对象,也就是整个HashEntry数组,所以又叫分段锁。对JDK1.8中的ConcurrentHashMap而言舍弃了分段锁的实现方式,元素都存在Node数组中,每次锁住的是一个Node对象,而不是某一段数组,所以支持的写的并发度更高。再者它引入了红黑树,在hash冲突严重时,读操作的效率更高。这两点便是JDK1.8对ConcurrentHashMap所做的主要优化。总结ConcurrentHashMap类的实现,可以说是并发容器中,经典中的经典。深入的理解这个类,需要数据结构的基础,以及并发编程的基础。
2022年02月21日
52 阅读
0 评论
0 点赞
2022-02-21
SpringBoot的启动流程源码解析
前言 在拥有 Spring Boot 以前,我们要运行一个 Java Web 应用,首先需要有一个 Web 容器(例如 Tomcat ),然后将我们的 Web 应用打包后放到容器的相应目录下,最后再启动容器。 在 IDE 中也需要对 Web 容器进行一些配置,才能够运行或者 Debug。而使用 Spring Boot 我们只需要像运行普通 JavaSE 程序一样,run 一下 main () 方法就可以启动一个 Web 应用了。追本溯源只需要下面几行代码我们就可以跑起一个 Web 服务器:@SpringBootApplication public class SpringbootApplication { public static void main(String[] args) { SpringApplication.run(SpringbootApplication.class, args); } }去掉类的声明和方法定义这些样板代码,核心代码就只有一个 @SpringBootApplication 注解和 SpringApplication.run (SpringbootApplication.class, args) 方法。而我们知道注解相当于是一种配置,那么这个 run () 方法必然就是 Spring Boot 的启动入口了。容器启动流程接下来,我们沿着 run () 方法来顺藤摸瓜。进入 SpringApplication 类,来看看 run () 方法的具体实现:public class SpringApplication { ...... public ConfigurableApplicationContext run(String... args) { // 1 应用启动计时开始 StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 2 声明上下文 ConfigurableApplicationContext context = null; // 3 初始化异常报告集合 Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); // 4 设置 java.awt.headless 属性 configureHeadlessProperty(); // 5 启动监听器 SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(); try { // 6 初始化默认应用参数 ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 7 准备应用环境 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); // 8 打印 Banner(Spring Boot 的 LOGO) Banner printedBanner = printBanner(environment); // 9 通过反射创建上下文实例 context = createApplicationContext(); // 10 构建异常报告 exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); // 11 构建上下文 prepareContext(context, environment, listeners, applicationArguments, printedBanner); // 12 刷新上下文 refreshContext(context); // 13 刷新上下文后处理 afterRefresh(context, applicationArguments); // 14 应用启动计时结束 stopWatch.stop(); if (this.logStartupInfo) { // 15 打印启动时间日志 new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } // 16 发布上下文启动完成事件 listeners.started(context); // 17 调用 runners callRunners(context, applicationArguments); } catch (Throwable ex) { // 18 应用启动发生异常后的处理 handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { // 19 发布上下文就绪事件 listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; } ...... }Spring Boot 启动时做的所有操作都这这个方法里面,当然在调用上面这个 run () 方法之前,还创建了一个 SpringApplication 的实例对象。因为上面这个 run () 方法并不是一个静态方法,所以需要一个对象实例才能被调用。可以看到,方法的返回值类型为ConfigurableApplicationContext,这是一个接口,我们真正得到的是 AnnotationConfigServletWebServerApplicationContext 的实例。通过类名我们可以知道,这是一个基于注解的 Servlet Web 应用上下文(上下文(context)是 Spring 中的核心概念)。创建上下文实例createApplicationContext下面我们来到 run () 方法中编号 9 的位置,这里调用了一个 createApplicationContext () 方法,点进去我们会看到它的代码如下:public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot." + "web.servlet.context.AnnotationConfigServletWebServerApplicationContext"; protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this.applicationContextClass; if (contextClass == null) { try { switch (this.webApplicationType) { case SERVLET: contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS); break; case REACTIVE: contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); break; default: contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); } } catch (ClassNotFoundException ex) { throw new IllegalStateException( "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex); } } return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); }这个方法就是根据 SpringBootApplication 的 webApplicationType 属性的值,利用反射来创建不同类型的应用上下文(context)。而属性 webApplicationType 的值是在前面执行构造方法的时候由WebApplicationType.deduceFromClasspath()获得的。通过方法名很容易看出来,就是根据 classpath 中的类来推断当前的应用类型。我们这里是一个普通的 Web 应用,所以最终返回的类型为 SERVLET。所以会通过反射加载DEFAULT_SERVLET_WEB_CONTEXT_CLASS,最后返回一个 AnnotationConfigServletWebServerApplicationContext实例(就像我们上文所说的那样)。构建容器上下文prepareContext接着我们来到 run () 方法编号 11 的 prepareContext () 方法。通过方法名,我们也能猜到它是为 context 做上台前的准备工作的。private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { ...... // 加载资源 load(context, sources.toArray(new Object[0])); listeners.contextLoaded(context); }在这个方法中,会做一些准备工作,包括初始化容器上下文、设置环境、加载资源等。加载资源 上面的代码中,又调用了一个很关键的方法 —— load ()。这个 load () 方法真正的作用是去调用 BeanDefinitionLoader 类的 load () 方法。源码如下:class BeanDefinitionLoader { ...... int load() { int count = 0; for (Object source : this.sources) { count += load(source); } return count; } private int load(Object source) { Assert.notNull(source, "Source must not be null"); if (source instanceof Class<?>) { return load((Class<?>) source); } if (source instanceof Resource) { return load((Resource) source); } if (source instanceof Package) { return load((Package) source); } if (source instanceof CharSequence) { return load((CharSequence) source); } throw new IllegalArgumentException("Invalid source type " + source.getClass()); } ...... }可以看到,load () 方法在加载 Spring 中各种资源。其中我们最熟悉的就是 load ((Class<?>) source) 和 load ((Package) source) 了。一个用来加载类,一个用来加载扫描的包。load ((Class<?>) source) 中会通过调用 isComponent () 方法来判断资源是否为 Spring 容器管理的组件。 isComponent () 方法通过资源是否包含 @Component 注解(@Controller、@Service、@Repository 等都包含在内)来区分是否为 Spring 容器管理的组件。而 load ((Package) source) 方法则是用来加载 @ComponentScan 注解定义的包路径。总结我们知道,Spring 是一个容器,我们喜欢它的一个重要原因就是它帮我们把 Bean 进行了统一的管理。Bean 的创建与销毁都由 Spring 来完成,而我们只需要关注使用,这也是 Spring IoC 的核心工作内容。到此,Spring 真正开始开展 Bean 管理的工作了,prepareContext () 方法把所有需要管理的 Bean 统计出来,在后面的 refreshContext () 方法中会进行更进一步的操作。 refreshContext () 方法和自动配置关系紧密。
2022年02月21日
66 阅读
0 评论
0 点赞
2022-02-21
记录一下最近学习到的【线程池】相关的知识
学习目标【理解】线程池基本概念【理解】线程池工作原理【掌握】java内置线程池【应用】使用java内置线程池完成综合案例线程池1.什么是线程池?答:线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到阻塞队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象。2.为什么要使用线程池?答:使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力。总结一下:①:降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。②:提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。③:提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控,避免不停的创建线程导致OOM。3.创建线程池的7大核心参数?答:我们查看源码可以看到,jdk 1.8之前创建线程池的方式,基本都是通过new ThreadPoolExecutor()去实现。public ThreadPoolExecutor( int corePoolSize, //核心线程数量 int maximumPoolSize,// 最大线程数 long keepAliveTime, // 最大空闲时间 TimeUnit unit, // 时间单位 BlockingQueue<Runnable> workQueue, // 任务队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 任务拒绝策略(饱和处理机制) ) corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程。核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定,例如:执行一个任务需要0.1秒,系统百分之80的时间每秒都会产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数为10。当然实际情况不可能这么平均,所以我们一般按照8020原则设计即可,既 按照百分之80的情况设计核心线程数,剩下的百分之20可以利用最大线程数处理 ;maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数。最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定:例如:上述环境中,如果系统每秒最大产生的任务是1000个,那么,最大线程数=(最大任务数-任务队列长度)单个任务执行时间;既:最大线程数=(1000-200)0.1=80个。keepAliveTime 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过setKeepAliveTime 来设置空闲时间。这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可。unit 空闲存活时间的时间类型。workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程。ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,比如用牛逼的GUAVA。一般我们会根据业务来制定不同的线程工厂。Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这时也就拒绝。4.线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源。2、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。线程池工作原理线程池中线程复用原理线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。Java内置线程池-ExecutorService介绍ExecutorService接口是java内置的线程池接口,通过学习接口中的方法,可以快速的掌握java内置线程池的基本使用。常用方法:void shutdown()启动一次顺序关闭,执行以前提交的任务,但不接受新任务。List shutdownNow()停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行的任务列表。 Future submit(Callable task) 执行带返回值的任务,返回一个Future对象。Future<?> submit(Runnable task) 执行 Runnable 任务,并返回一个表示该任务的Future。 Future submit(Runnable task, T result) 执行 Runnable 任务,并返回一个表示该任务的Future。Java内置线程池-ExecutorService获取获取ExecutorService可以利用JDK中的Executors 类中的静态方法,常用获取方式如下:static ExecutorService newCachedThreadPool()创建一个默认的线程池对象,里面的线程可重用,且在第一次使用时才创建static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)线程池中的所有线程都使用ThreadFactory来创建,这样的线程无需手动启动,自动执行;static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)创建一个可重用固定线程数的线程池且线程池中的所有线程都使用ThreadFactory来创建。static ExecutorService newSingleThreadExecutor()创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)创建一个使用单个 worker 线程的 Executor,且线程池中的所有线程都使用ThreadFactory来创建。综合案例-秒杀商品案例介绍: 假如某网上商城推出活动,新上架10部新手机免费送客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀挣抢,假如有20人同时参与了该活动,请使用线程池模拟这个场景,保证前10人秒杀成功,后10人秒杀失败;要求: 使用线程池创建线程解决线程安全问题思路提示: 既然商品总数量是10个,那么我们可以在创建线程池的时候初始化线程数是10个及以下,设计线程池最大数量为10个;当某个线程执行完任务之后,可以让其他秒杀的人继续使用该线程参与秒杀;使用synchronized控制线程安全,防止出现错误数据;代码步骤: 编写任务类,主要是送出手机给秒杀成功的客户;编写主程序类,创建20个任务(模拟20个客户);创建线程池对象并接收20个任务,开始执行任务;/** * @author lvwei */ public class ThreadPollLearning { /** * 案例介绍: * 假如某网上商城推出活动,新上架10部新手机免费送客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀挣抢, * 假如有20人同时参与了该活动,请使用线程池模拟这个场景,保证前10人秒杀成功,后10人秒杀失败; * <p> * 要求: * 使用线程池创建线程 * 解决线程安全问题 * <p> * 思路提示: * 既然商品总数量是10个,那么我们可以在创建线程池的时候初始化线程数是10个及以下,设计线程池最大数量为10个; * 当某个线程执行完任务之后,可以让其他秒杀的人继续使用该线程参与秒杀; * 使用synchronized控制线程安全,防止出现错误数据; * <p> * 代码步骤: * 编写任务类,主要是送出手机给秒杀成功的客户; * 编写主程序类,创建20个任务(模拟20个客户); * 创建线程池对象并接收20个任务,开始执行任务; * * @param args */ public class ThreadPollLearning { public static void main(String[] args) { //1:创建一个线程池对象 ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(15)); //2:循环创建任务对象 for (int i = 1; i <= 20; i++) { MyTask myTask = new MyTask("客户" + i); pool.submit(myTask); } //3:关闭线程池 pool.shutdown(); } } public class MyTask implements Runnable { //设计一个变量,用于表示商品的数量 private static int id = 10; //表示客户名称的变量 private String userName; public MyTask(String userName) { this.userName = userName; } @Override public void run() { String name = Thread.currentThread().getName(); System.out.println(userName+"正在使用"+name+"参与秒杀任务..."); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (MyTask.class){ if(id>0){ System.out.println(userName+"使用"+name+"秒杀:"+id-- +"号商品成功啦!"); }else { System.out.println(userName+"使用"+name+"秒杀失败啦!"); } } } }
2022年02月21日
64 阅读
0 评论
0 点赞
2022-02-17
整合Elasticsearch实现商品搜索
Spring Data ElasticsearchSpring Data Elasticsearch是Spring提供的一种以Spring Data风格来操作数据存储的方式,它可以避免编写大量的样板代码。常用注解@Document// 标示映射到Elasticsearch文档上的领域对象 public @interface Document { // 索引库名次,mysql中数据库的概念 String indexName(); // 类型,mysql中表的概念 String type() default ""; // 默认分片数 short shards() default 5; // 默认副本数量 short replicas() default 1; }@Id// 表示是文档的id,文档可以认为是mysql中表行的概念 public @interface Id { }@Fieldpublic @interface Field { // 文档中字段的类型 FieldType type() default FieldType.Auto; // 是否建立倒排索引 boolean index() default true; // 是否进行存储 boolean store() default false; // 分词器名次 String analyzer() default ""; }//为文档自动指定元数据类型 public enum FieldType { Text,//会进行分词并建了索引的字符类型 Integer, Long, Date, Float, Double, Boolean, Object, Auto,//自动判断字段类型 Nested,//嵌套对象类型 Ip, Attachment, Keyword//不会进行分词建立索引的类型 }Sping Data方式的数据操作继承ElasticsearchRepository接口可以获得常用的数据操作方法可以使用衍生查询在接口中直接指定查询方法名称便可查询,无需进行实现,如商品表中有商品名称、标题和关键字,直接定义以下查询,就可以对这三个字段进行全文搜索。 /** * 搜索查询 * * @param name 商品名称 * @param subTitle 商品标题 * @param keywords 商品关键字 * @param page 分页信息 * @return */ Page<EsProduct> findByNameOrSubTitleOrKeywords(String name, String subTitle, String keywords, Pageable page);在idea中直接会提示对应字段使用@Query注解可以用Elasticsearch的DSL语句进行查询@Query("{"bool" : {"must" : {"field" : {"name" : "?0"}}}}") Page<EsProduct> findByName(String name,Pageable pageable);
2022年02月17日
74 阅读
0 评论
0 点赞
2022-02-17
缓存穿透、雪崩、击穿
缓存穿透定义: 指查询一个一定不存在的数据,由于缓存是不命中的,将去查询数据库,但数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层查询,失去了缓存的意义。造成缓存穿透的基本原因有两个:第一, 自身业务代码或者数据出现问题。第二, 一些恶意攻击、 爬虫等造成大量空命中。风险: 利用不存在的数据进行攻击,数据库瞬时压力增大,系统崩溃解决: 1、null结果缓存,并加入短暂的过期时间。 String get(String key) { // 从缓存中获取数据 String cacheValue = cache.get(key); // 缓存为空 if (StringUtils.isBlank(cacheValue)) { // 从存储中获取 String storageValue = storage.get(key); cache.set(key, storageValue); // 如果存储数据为空, 需要设置一个过期时间(300秒) if (storageValue == null) { cache.expire(key, 60 * 5); } return storageValue; } else { // 缓存非空 return cacheValue; } }2、布隆过滤器对于恶意攻击,向服务器请求大量不存在的数据造成的缓存穿透,还可以用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往后端发送。当布隆过滤器说 某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。 布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组比较稀疏,这个概率就会很大,如果这个位数组比较拥挤,这个概率就会降低。这种方法适用于数据命中不高、 数据相对固定、 实时性低(通常是数据集较大) 的应用场景, 代码维护较为复杂, 但是缓存空间占用很少。 示例伪代码: package com.redisson; import org.redisson.Redisson; import org.redisson.api.RBloomFilter; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class RedissonBloomFilter { public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://localhost:6379"); //构造Redisson RedissonClient redisson = Redisson.create(config); RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList"); //初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小 bloomFilter.tryInit(100000000L,0.03); //将zhuge插入到布隆过滤器中 bloomFilter.add("zhuge"); //判断下面号码是否在布隆过滤器中 System.out.println(bloomFilter.contains("guojia"));//false System.out.println(bloomFilter.contains("baiqi"));//false System.out.println(bloomFilter.contains("zhuge"));//true } }使用布隆过滤器需要把所有数据提前放入布隆过滤器,并且在增加数据时也要往布隆过滤器里放,布隆过滤器缓存过滤伪代码: //初始化布隆过滤器 RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList"); //初始化布隆过滤器:预计元素为100000000L,误差率为3% bloomFilter.tryInit(100000000L,0.03); //把所有数据存入布隆过滤器 void init(){ for (String key: keys) { bloomFilter.put(key); } } String get(String key) { // 从布隆过滤器这一级缓存判断下key是否存在 Boolean exist = bloomFilter.contains(key); if(!exist){ return ""; } // 从缓存中获取数据 String cacheValue = cache.get(key); // 缓存为空 if (StringUtils.isBlank(cacheValue)) { // 从存储中获取 String storageValue = storage.get(key); cache.set(key, storageValue); // 如果存储数据为空, 需要设置一个过期时间(300秒) if (storageValue == null) { cache.expire(key, 60 * 5); } return storageValue; } else { // 缓存非空 return cacheValue; } } 注意:布隆过滤器不能删除数据,如果要删除得重新初始化数据。缓存雪崩定义: 设置的key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部进入DB解决: 在原有的失效时间的基础上增加一个随机值缓存击穿定义: 某个key是一个热点数据,如果大量这个key在大量请求的时候正好失效解决: 加锁,大量并发只让一个去查,其他等待,查询得到数据后加入到缓存。热点缓存key重建优化开发人员使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现, 可能就会对应用造成致命的危害:当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。在缓存失效的瞬间, 有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃。要解决这个问题主要就是要避免大量线程同时重建缓存。我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。示例伪代码: String get(String key) { // 从Redis中获取数据 String value = redis.get(key); // 如果value为空, 则开始重构缓存 if (value == null) { // 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex String mutexKey = "mutext:key:" + key; if (redis.set(mutexKey, "1", "ex 180", "nx")) { // 从数据源获取数据 value = db.get(key); // 回写Redis, 并设置过期时间 redis.setex(key, timeout, value); // 删除key_mutex redis.delete(mutexKey); }// 其他线程休息50毫秒后重试 else { Thread.sleep(50); get(key); } } return value; }缓存与数据库双写不一致在大并发下,同时操作数据库与缓存会存在数据不一致性问题1、双写不一致情况2、读写并发不一致 解决方案: 1、对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。2、就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。3、如果不能容忍缓存数据不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。4、也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。总结:以上我们针对的都是读多写少的情况加入缓存提高性能,如果写多读多的情况又不能容忍缓存数据不一致,那就没必要加缓存了,可以直接操作数据库。当然,如果数据库抗不住压力,还可以把缓存作为数据读写的主存储,异步将数据同步到数据库,数据库只是作为数据的备份。放入缓存的数据应该是对实时性、一致性要求不是很高的数据。切记不要为了用缓存,同时又要保证绝对的一致性做大量的过度设计和控制,增加系统复杂性!
2022年02月17日
67 阅读
0 评论
0 点赞
2022-02-15
JAVA多线程并发2-四种线程池
一:四种线程的创建方式Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。1.1 newCachedThreadPool创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。1.2 newFixedThreadPool创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。1.3 newScheduledThreadPool创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3); scheduledThreadPool.schedule(new Runnable() { @Override public void run() { System.out.println("延迟3秒...."); } }, 3, TimeUnit.SECONDS); scheduledThreadPool.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("延迟1秒后,每3秒执行一次...."); } }, 1, 3, TimeUnit.SECONDS); }1.4 newSingleThreadExecutorExecutors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!二:线程生命周期(状态)当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。2.1 新建状态(NEW)当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。2.2 就绪状态(RUNNABLE)当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。2.3 运行状态(RUNNING)如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。2.4 阻塞状态(BLOCKED)阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:等待阻塞(o.wait->等待对列):运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。同步阻塞(lock->锁池)运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。其他阻塞(sleep/join)运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。2.5 线程死亡(DEAD)线程会以下面三种方式结束,结束后就是死亡状态。 正常结束run()或 call()方法执行完成,线程正常结束。 异常结束线程抛出一个未捕获的 Exception 或 Error。 调用 stop直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。2.6 结束线程的方式2.6.1 正常运行结束程序运行结束,线程自动结束。2.6.2 使用退出标志退出线程一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while循环是否退出,代码示例:public class ThreadSafe extends Thread { public volatile boolean exit = false; public void run() { while (!exit){ //do something } } }定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。2.6.3 Interrupt 方法结束线程使用 interrupt()方法来中断线程有两种情况:线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。public class ThreadSafe extends Thread { public void run() { while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出 try{ Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出 }catch(InterruptedException e){ e.printStackTrace(); break;//捕获到异常之后,执行 break 跳出循环 } } } }2.6.4 stop 方法终止线程(线程不安全)程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。
2022年02月15日
66 阅读
0 评论
0 点赞
2022-02-15
JAVA 多线程并发1-多线程的创建方式
1.并发知识库2.JAVA线程实现/创建的方式2.1 通过继承Thread实现多线程Thread类本质是实现了runnable接口的一个实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native方法,它将启动一个新线程,并执行 run()方法。/** * 时间 2022/2/15 10:04 * 作者 lvwei * 邮箱 lvwei@bdysoft.com * 版本 1.0 */ class testThread { public void main(String[] args) { MyThread myThread = new MyThread("A"); myThread.start(); } } class MyThread extends Thread { private String name; public MyThread(String name) { this.name=name; } @Override public void run() { System.out.println("通过继承Thread实现多线程...."); } }2.2 通过实现runable接口实现多线程通过2.1我们了解到Thread类是实现了Runable使自己具备子线程能力,那么同理我们自己的类也可以实现Runable接口来使自己实现子线程。 public void test() { System.out.println("主线程:" + Thread.currentThread().getId()); MyThread2 myThread2 = new MyThread2(); new Thread(myThread2).start(); } class MyThread2 implements Runnable { @Override public void run() { System.out.println("子线程:" + Thread.currentThread().getId() + "===>通过实现Runable实现多线程...."); } }2.3 ExecutorService、Callable、Future 有返回值线程有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。//创建一个线程池 ExecutorService pool = Executors.newFixedThreadPool(taskSize); // 创建多个有返回值的任务 List<Future> list = new ArrayList<Future>(); for (int i = 0; i < taskSize; i++) { Callable c = new MyCallable(i + " "); // 执行任务并获取 Future 对象 Future f = pool.submit(c); list.add(f); } // 关闭线程池 pool.shutdown(); // 获取所有并发任务的运行结果 for (Future f : list) { // 从 Future 对象上获取任务的返回值,并输出到控制台 System.out.println("res:" + f.get().toString()); }2.4 基于线程池的方式线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。 // 创建一个线程池 ExecutorService pool = Executors.newFixedThreadPool(5); // 创建多个有返回值的任务 ArrayList<Future> futureArrayList = new ArrayList<>(); for (int i = 0; i < 5; i++) { Callable callable = new Callable() { @Override public Object call() throws Exception { return "当前执行的线程为:" + Thread.currentThread().getId(); } }; // 执行任务并获取 Future 对象 Future future = pool.submit(callable); futureArrayList.add(future); } // 关闭线程池 pool.shutdown(); // 获取所有并发的运行结果 for (Future future : futureArrayList) { // 从 Future 对象上获取任务的返回值,并输出到控制 System.out.println(future.get().toString()); }
2022年02月15日
67 阅读
0 评论
0 点赞
2022-02-14
MQ相关(RabbitMQ)
1. MQ的优点答:异步处理,应用解耦,流量削峰,日志处理,消息通讯。2. MQ的缺点答:系统可用性降低,系统复杂度增加,一致性问题。3. Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点?4. 如何处理消息重复消费问题?答:消费端处理消息的业务逻辑需要保持幂等性,保证每条消息都有唯一的编号(消息ID),利用一张日志表记录消息处理的结果。5. RabbitMQ相关概念标志中文名英文名描述P生产者Producer消息的发送者,可以将消息发送到交换机C消费者Consumer消息的接收者,从队列中获取消息并进行消费X交换机Exchange接收生产者发送的消息,并根据路由键发送给指定队列Q队列Queue存储从交换机发来的消息type交换机类型type不同类型的交换机转发消息方式不同fanout发布/订阅模式fanout广播消息给所有绑定交换机的队列direct路由模式direct根据路由键发送消息topic通配符模式topic根据路由键的匹配规则发送消息6. RabbitMQ 五种工作模式答:简单模式,工作模式,发布订阅模式,路由模式,通配符(主题)模式。参考我的另一篇文章: RabbitMQ的5种工作模式7. 如何保证RabbitMQ消息的顺序性?答:拆分多个 queue(消息队列),每个 queue(消息队列) 一个 consumer(消费者),就是多一些 queue(消息队列)而已,确实是麻烦点;或者就一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理。8. 如何保证消息不丢失?答:按照三种场景回答:生产者发送消息给MQ的时候丢失。在生产者端开启confirm模式,每次写的消息会分配一个唯一的id,如果写入了MQ,MQ会返回ack信息。MQ内部丢失,暂存内存中,还没消费,自己挂掉,数据会都丢失。MQ设置为持久化。将内存数据持久化到磁盘中消费者接收后未处理,消费者DIE,消息丢失利用MQ提供的ACK机制,关闭MQ的自动ACK,消费者端如果没有ack的话,MQ会重新分配给其他的consumer,不过要保证消息不被重复消费。9. 消息基于什么传输?答:由于TCP连接的创建和消费开销较大,切并发收系统资源的限制,有性能瓶颈。RabbitMQ使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。10. 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,怎么办?答:消息积压处理办法:临时紧急扩容先修复 consumer 的问题,确保其恢复消费速度,然后将现有 cnosumer 都停掉。新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。MQ中消息失效:假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。mq消息队列块满了:如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。
2022年02月14日
40 阅读
0 评论
0 点赞
2022-01-27
连RabbitMQ的5种核心消息模式都不懂,也敢说自己会用消息队列!
以前看过的关于RabbitMQ核心消息模式的文章都是基于Java API的,最近看了下官方文档,发现这些核心消息模式都可以通过Spring AMQP来实现。于是总结了下RabbitMQ的核心知识点,包括RabbitMQ在Windows和Linux下的安装、5种核心消息模式的Spring AMQP实现,相信对于想要学习和回顾RabbitMQ的朋友都会有所帮助。简介RabbitMQ是最受欢迎的开源消息中间件之一,在全球范围内被广泛应用。RabbitMQ是轻量级且易于部署的,能支持多种消息协议。RabbitMQ可以部署在分布式系统中,以满足大规模、高可用的要求。相关概念我们先来了解下RabbitMQ中的相关概念,这里以5种消息模式中的路由模式为例。标志中文名英文名描述P生产者Producer消息的发送者,可以将消息发送到交换机C消费者Consumer消息的接收者,从队列中获取消息并进行消费X交换机Exchange接收生产者发送的消息,并根据路由键发送给指定队列Q队列Queue存储从交换机发来的消息type交换机类型type不同类型的交换机转发消息方式不同fanout发布/订阅模式fanout广播消息给所有绑定交换机的队列direct路由模式direct根据路由键发送消息topic通配符模式topic根据路由键的匹配规则发送消息5种消息模式这5种消息模式是构建基于RabbitMQ的消息应用的基础,一定要牢牢掌握它们。学过RabbitMQ的朋友应该了解过这些消息模式的Java实现,这里我们使用Spring AMQP的形式来实现它们。简单模式简单模式是最简单的消息模式,它包含一个生产者、一个消费者和一个队列。生产者向队列里发送消息,消费者从队列中获取消息并消费。 模式示意图 Spring AMQP实现首先需要在pom.xml中添加Spring AMQP的相关依赖;<!--Spring AMQP依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency然后修改application.yml,添加RabbitMQ的相关配置;spring: rabbitmq: host: localhost port: 5672 virtual-host: /mall username: mall password: mall publisher-confirms: true #消息发送到交换器确认 publisher-returns: true #消息发送到队列确认添加简单模式相关Java配置,创建一个名为simple.hello的队列、一个生产者和一个消费者;@Configuration public class SimpleRabbitConfig { @Bean public Queue hello() { return new Queue("simple.hello"); } @Bean public SimpleSender simpleSender(){ return new SimpleSender(); } @Bean public SimpleReceiver simpleReceiver(){ return new SimpleReceiver(); } }生产者通过send方法向队列simple.hello中发送消息;public class SimpleSender { private static final Logger LOGGER = LoggerFactory.getLogger(SimpleSender.class); @Autowired private RabbitTemplate template; private static final String queueName="simple.hello"; public void send() { String message = "Hello World!"; this.template.convertAndSend(queueName, message); LOGGER.info(" [x] Sent '{}'", message); } }消费者从队列simple.hello中获取消息;@RabbitListener(queues = "simple.hello") public class SimpleReceiver { private static final Logger LOGGER = LoggerFactory.getLogger(SimpleReceiver.class); @RabbitHandler public void receive(String in) { LOGGER.info(" [x] Received '{}'", in); } }在controller中添加测试接口,调用该接口开始发送消息;@Api(tags = "RabbitController", description = "RabbitMQ功能测试") @Controller @RequestMapping("/rabbit") public class RabbitController { @Autowired private SimpleSender simpleSender; @ApiOperation("简单模式") @RequestMapping(value = "/simple", method = RequestMethod.GET) @ResponseBody public CommonResult simpleTest() { for(int i=0;i<10;i++){ simpleSender.send(); ThreadUtil.sleep(1000); } return CommonResult.success(null); } }运行后结果如下,可以发现生产者往队列中发送消息,消费者从队列中获取消息并消费。工作模式工作模式是指向多个互相竞争的消费者发送消息的模式,它包含一个生产者、两个消费者和一个队列。两个消费者同时绑定到一个队列上去,当消费者获取消息处理耗时任务时,空闲的消费者从队列中获取并消费消息。 模式示意图 添加工作模式相关Java配置,创建一个名为work.hello的队列、一个生产者和两个消费者;@Configuration public class WorkRabbitConfig { @Bean public Queue workQueue() { return new Queue("work.hello"); } @Bean public WorkReceiver workReceiver1() { return new WorkReceiver(1); } @Bean public WorkReceiver workReceiver2() { return new WorkReceiver(2); } @Bean public WorkSender workSender() { return new WorkSender(); } }生产者通过send方法向队列work.hello中发送消息,消息中包含一定数量的.号;public class WorkSender { private static final Logger LOGGER = LoggerFactory.getLogger(WorkSender.class); @Autowired private RabbitTemplate template; private static final String queueName = "work.hello"; public void send(int index) { StringBuilder builder = new StringBuilder("Hello"); int limitIndex = index % 3+1; for (int i = 0; i < limitIndex; i++) { builder.append('.'); } builder.append(index+1); String message = builder.toString(); template.convertAndSend(queueName, message); LOGGER.info(" [x] Sent '{}'", message); } }两个消费者从队列work.hello中获取消息,名称分别为instance 1和instance 2,消息中包含.号越多,耗时越长;@RabbitListener(queues = "work.hello") public class WorkReceiver { private static final Logger LOGGER = LoggerFactory.getLogger(WorkReceiver.class); private final int instance; public WorkReceiver(int i) { this.instance = i; } @RabbitHandler public void receive(String in) { StopWatch watch = new StopWatch(); watch.start(); LOGGER.info("instance {} [x] Received '{}'", this.instance, in); doWork(in); watch.stop(); LOGGER.info("instance {} [x] Done in {}s", this.instance, watch.getTotalTimeSeconds()); } private void doWork(String in) { for (char ch : in.toCharArray()) { if (ch == '.') { ThreadUtil.sleep(1000); } } } }在controller中添加测试接口,调用该接口开始发送消息;@Api(tags = "RabbitController", description = "RabbitMQ功能测试") @Controller @RequestMapping("/rabbit") public class RabbitController { @Autowired private WorkSender workSender; @ApiOperation("工作模式") @RequestMapping(value = "/work", method = RequestMethod.GET) @ResponseBody public CommonResult workTest() { for(int i=0;i<10;i++){ workSender.send(i); ThreadUtil.sleep(1000); } return CommonResult.success(null); } }运行后结果如下,可以发现生产者往队列中发送包含不同数量.号的消息,instance 1和instance 2消费者互相竞争,分别消费了一部分消息。发布/订阅模式发布/订阅模式是指同时向多个消费者发送消息的模式(类似广播的形式),它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列绑定到交换机上去,生产者通过发送消息到交换机,所有消费者接收并消费消息。模式示意图Spring AMQP实现添加发布/订阅模式相关Java配置,创建一个名为exchange.fanout的交换机、一个生产者、两个消费者和两个匿名队列,将两个匿名队列都绑定到交换机;@Configuration public class FanoutRabbitConfig { @Bean public FanoutExchange fanout() { return new FanoutExchange("exchange.fanout"); } @Bean public Queue fanoutQueue1() { return new AnonymousQueue(); } @Bean public Queue fanoutQueue2() { return new AnonymousQueue(); } @Bean public Binding fanoutBinding1(FanoutExchange fanout, Queue fanoutQueue1) { return BindingBuilder.bind(fanoutQueue1).to(fanout); } @Bean public Binding fanoutBinding2(FanoutExchange fanout, Queue fanoutQueue2) { return BindingBuilder.bind(fanoutQueue2).to(fanout); } @Bean public FanoutReceiver fanoutReceiver() { return new FanoutReceiver(); } @Bean public FanoutSender fanoutSender() { return new FanoutSender(); } }添加发布/订阅模式相关Java配置,创建一个名为exchange.fanout的交换机、一个生产者、两个消费者和两个匿名队列,将两个匿名队列都绑定到交换机;public class FanoutSender { private static final Logger LOGGER = LoggerFactory.getLogger(FanoutSender.class); @Autowired private RabbitTemplate template; private static final String exchangeName = "exchange.fanout"; public void send(int index) { StringBuilder builder = new StringBuilder("Hello"); int limitIndex = index % 3 + 1; for (int i = 0; i < limitIndex; i++) { builder.append('.'); } builder.append(index + 1); String message = builder.toString(); template.convertAndSend(exchangeName, "", message); LOGGER.info(" [x] Sent '{}'", message); } }消费者从绑定的匿名队列中获取消息,消息中包含.号越多,耗时越长,由于该消费者可以从两个队列中获取并消费消息,可以看做两个消费者,名称分别为instance 1和instance 2public class FanoutReceiver { private static final Logger LOGGER = LoggerFactory.getLogger(FanoutReceiver.class); @RabbitListener(queues = "#{fanoutQueue1.name}") public void receive1(String in) { receive(in, 1); } @RabbitListener(queues = "#{fanoutQueue2.name}") public void receive2(String in) { receive(in, 2); } private void receive(String in, int receiver) { StopWatch watch = new StopWatch(); watch.start(); LOGGER.info("instance {} [x] Received '{}'", receiver, in); doWork(in); watch.stop(); LOGGER.info("instance {} [x] Done in {}s", receiver, watch.getTotalTimeSeconds()); } private void doWork(String in) { for (char ch : in.toCharArray()) { if (ch == '.') { ThreadUtil.sleep(1000); } } } }在controller中添加测试接口,调用该接口开始发送消息;@Api(tags = "RabbitController", description = "RabbitMQ功能测试") @Controller @RequestMapping("/rabbit") public class RabbitController { @Autowired private FanoutSender fanoutSender; @ApiOperation("发布/订阅模式") @RequestMapping(value = "/fanout", method = RequestMethod.GET) @ResponseBody public CommonResult fanoutTest() { for(int i=0;i<10;i++){ fanoutSender.send(i); ThreadUtil.sleep(1000); } return CommonResult.success(null); } }运行后结果如下,可以发现生产者往队列中发送包含不同数量.号的消息,instance 1和instance 2同时获取并消费了消息。路由模式路由模式是可以根据路由键选择性给多个消费者发送消息的模式,它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列通过路由键绑定到交换机上去,生产者发送消息到交换机,交换机通过路由键转发到不同队列,队列绑定的消费者接收并消费消息。模式示意图 Spring AMQP实现添加路由模式相关Java配置,创建一个名为exchange.direct的交换机、一个生产者、两个消费者和两个匿名队列,队列通过路由键都绑定到交换机,队列1的路由键为orange和black,队列2的路由键为green和black;@Configuration public class DirectRabbitConfig { @Bean public DirectExchange direct() { return new DirectExchange("exchange.direct"); } @Bean public Queue directQueue1() { return new AnonymousQueue(); } @Bean public Queue directQueue2() { return new AnonymousQueue(); } @Bean public Binding directBinding1a(DirectExchange direct, Queue directQueue1) { return BindingBuilder.bind(directQueue1).to(direct).with("orange"); } @Bean public Binding directBinding1b(DirectExchange direct, Queue directQueue1) { return BindingBuilder.bind(directQueue1).to(direct).with("black"); } @Bean public Binding directBinding2a(DirectExchange direct, Queue directQueue2) { return BindingBuilder.bind(directQueue2).to(direct).with("green"); } @Bean public Binding directBinding2b(DirectExchange direct, Queue directQueue2) { return BindingBuilder.bind(directQueue2).to(direct).with("black"); } @Bean public DirectReceiver receiver() { return new DirectReceiver(); } @Bean public DirectSender directSender() { return new DirectSender(); } }生产者通过send方法向交换机exchange.direct中发送消息,发送时使用不同的路由键,根据路由键会被转发到不同的队列;public class DirectSender { @Autowired private RabbitTemplate template; private static final String exchangeName = "exchange.direct"; private final String[] keys = {"orange", "black", "green"}; private static final Logger LOGGER = LoggerFactory.getLogger(DirectSender.class); public void send(int index) { StringBuilder builder = new StringBuilder("Hello to "); int limitIndex = index % 3; String key = keys[limitIndex]; builder.append(key).append(' '); builder.append(index+1); String message = builder.toString(); template.convertAndSend(exchangeName, key, message); LOGGER.info(" [x] Sent '{}'", message); } }消费者从自己绑定的匿名队列中获取消息,由于该消费者可以从两个队列中获取并消费消息,可以看做两个消费者,名称分别为instance 1和instance 2;public class DirectReceiver { private static final Logger LOGGER = LoggerFactory.getLogger(DirectReceiver.class); @RabbitListener(queues = "#{directQueue1.name}") public void receive1(String in){ receive(in, 1); } @RabbitListener(queues = "#{directQueue2.name}") public void receive2(String in){ receive(in, 2); } private void receive(String in, int receiver){ StopWatch watch = new StopWatch(); watch.start(); LOGGER.info("instance {} [x] Received '{}'", receiver, in); doWork(in); watch.stop(); LOGGER.info("instance {} [x] Done in {}s", receiver, watch.getTotalTimeSeconds()); } private void doWork(String in){ for (char ch : in.toCharArray()) { if (ch == '.') { ThreadUtil.sleep(1000); } } } }在controller中添加测试接口,调用该接口开始发送消息;@Api(tags = "RabbitController", description = "RabbitMQ功能测试") @Controller @RequestMapping("/rabbit") public class RabbitController { @Autowired private DirectSender directSender; @ApiOperation("路由模式") @RequestMapping(value = "/direct", method = RequestMethod.GET) @ResponseBody public CommonResult directTest() { for(int i=0;i<10;i++){ directSender.send(i); ThreadUtil.sleep(1000); } return CommonResult.success(null); } }运行后结果如下,可以发现生产者往队列中发送包含不同路由键的消息,instance 1获取到了orange和black消息,instance 2获取到了green和black消息。通配符模式通配符模式是可以根据路由键匹配规则选择性给多个消费者发送消息的模式,它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列通过路由键匹配规则绑定到交换机上去,生产者发送消息到交换机,交换机通过路由键匹配规则转发到不同队列,队列绑定的消费者接收并消费消息。特殊匹配符号'*':只能匹配一个单词;'#':可以匹配零个或多个单词。 模式示意图 Spring AMQP实现添加通配符模式相关Java配置,创建一个名为exchange.topic的交换机、一个生产者、两个消费者和两个匿名队列,匹配.orange.和..rabbit发送到队列1,匹配lazy.#发送到队列2;@Configuration public class TopicRabbitConfig { @Bean public TopicExchange topic() { return new TopicExchange("exchange.topic"); } @Bean public Queue topicQueue1() { return new AnonymousQueue(); } @Bean public Queue topicQueue2() { return new AnonymousQueue(); } @Bean public Binding topicBinding1a(TopicExchange topic, Queue topicQueue1) { return BindingBuilder.bind(topicQueue1).to(topic).with("*.orange.*"); } @Bean public Binding topicBinding1b(TopicExchange topic, Queue topicQueue1) { return BindingBuilder.bind(topicQueue1).to(topic).with("*.*.rabbit"); } @Bean public Binding topicBinding2a(TopicExchange topic, Queue topicQueue2) { return BindingBuilder.bind(topicQueue2).to(topic).with("lazy.#"); } @Bean public TopicReceiver topicReceiver() { return new TopicReceiver(); } @Bean public TopicSender topicSender() { return new TopicSender(); } }生产者通过send方法向交换机exchange.topic中发送消息,消息中包含不同的路由键;public class TopicSender { @Autowired private RabbitTemplate template; private static final String exchangeName = "exchange.topic"; private static final Logger LOGGER = LoggerFactory.getLogger(TopicSender.class); private final String[] keys = {"quick.orange.rabbit", "lazy.orange.elephant", "quick.orange.fox", "lazy.brown.fox", "lazy.pink.rabbit", "quick.brown.fox"}; public void send(int index) { StringBuilder builder = new StringBuilder("Hello to "); int limitIndex = index%keys.length; String key = keys[limitIndex]; builder.append(key).append(' '); builder.append(index+1); String message = builder.toString(); template.convertAndSend(exchangeName, key, message); LOGGER.info(" [x] Sent '{}'",message); System.out.println(" [x] Sent '" + message + "'"); } }消费者从自己绑定的匿名队列中获取消息,由于该消费者可以从两个队列中获取并消费消息,可以看做两个消费者,名称分别为instance 1和instance 2;public class TopicReceiver { private static final Logger LOGGER = LoggerFactory.getLogger(TopicReceiver.class); @RabbitListener(queues = "#{topicQueue1.name}") public void receive1(String in){ receive(in, 1); } @RabbitListener(queues = "#{topicQueue2.name}") public void receive2(String in){ receive(in, 2); } public void receive(String in, int receiver){ StopWatch watch = new StopWatch(); watch.start(); LOGGER.info("instance {} [x] Received '{}'", receiver, in); doWork(in); watch.stop(); LOGGER.info("instance {} [x] Done in {}s", receiver, watch.getTotalTimeSeconds()); } private void doWork(String in){ for (char ch : in.toCharArray()) { if (ch == '.') { ThreadUtil.sleep(1000); } } } }在controller中添加测试接口,调用该接口开始发送消息;@Api(tags = "RabbitController", description = "RabbitMQ功能测试") @Controller @RequestMapping("/rabbit") public class RabbitController { @Autowired private TopicSender topicSender; @ApiOperation("通配符模式") @RequestMapping(value = "/topic", method = RequestMethod.GET) @ResponseBody public CommonResult topicTest() { for(int i=0;i<10;i++){ topicSender.send(i); ThreadUtil.sleep(1000); } return CommonResult.success(null); } }运行后结果如下,可以发现生产者往队列中发送包含不同路由键的消息,instance 1和instance 2分别获取到了匹配的消息。
2022年01月27日
42 阅读
0 评论
0 点赞
2021-11-15
单体应用并发实际场景(保持余额操做的正确:数据库余额字段版)
{card-describe title="场景"} 我的在一家银行办了一个帐户,银行给了 一张卡(存取款)、一本存折(存取款)、一个网银(查询余额)java卡和存储不断存款和取款,网银不断查询余额。{/card-describe}数据库余额表:本来想用版本号来实现的,后面弃用version字段。DROP TABLE IF EXISTS `t_test`; CREATE TABLE `t_test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `account` decimal(11,2) DEFAULT NULL, `version` int(100) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO `t_test` VALUES ('1', '50.00', '1');mapper.xml文件:仔细看两个sql的写法,这里是重点,请不要在java代码中进行余额的加减操做。<?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.taotao.mapper.TTestMapper" > <resultMap id="BaseResultMap" type="com.taotao.pojo.TTest" > <id column="id" property="id" jdbcType="INTEGER" /> <result column="account" property="account" jdbcType="DECIMAL" /> <result column="version" property="version" jdbcType="INTEGER" /> </resultMap> <update id="updateAccountAdd" parameterType="com.taotao.pojo.TTest" > update t_test set account = account + #{newAccount,jdbcType=DECIMAL} where id = #{id,jdbcType=INTEGER} </update> <update id="updateAccountSub" parameterType="com.taotao.pojo.TTest" > update t_test set account = account - #{newAccount,jdbcType=DECIMAL} where id = #{id,jdbcType=INTEGER} and account >= #{newAccount,jdbcType=DECIMAL} </update> </mapper>service:请在每一个方法上加入事物和synchronized。@Service public class TestServiceImpl implements TestService { @Autowired private TTestMapper testMapper; /** * 存钱 * * @param money */ @Override @Transactional public synchronized BigDecimal addAcount(String name, int money) throws TransactionalException { TTest tTest = testMapper.selectByPrimaryKey(1); tTest.setNewAccount(new BigDecimal(money)); int i = testMapper.updateAccountAdd(tTest); if (i == 0){ System.out.println("添加余额失败!余额=" + tTest.getAccount()); return new BigDecimal(money); } System.out.println(name + "...存入:" + money + "..." + Thread.currentThread().getName()); return selectAcount(name); } /** * 取钱 * * @param money */ @Override @Transactional public synchronized BigDecimal subAcount(String name, int money) throws TransactionalException{ TTest tTest = testMapper.selectByPrimaryKey(1); tTest.setNewAccount(new BigDecimal(money)); int i = testMapper.updateAccountSub(tTest); if (i == 0){ System.out.println("帐户余额不足!余额=" + tTest.getAccount()); return new BigDecimal(money); } System.out.println(name + "...取出:" + money + "..." + Thread.currentThread().getName()); return selectAcount(name); } /** * 查询余额 */ @Override @Transactional public synchronized BigDecimal selectAcount(String name) throws TransactionalException{ TTest tTest = testMapper.selectByPrimaryKey(1); System.out.println(name + "...余额:" + tTest.getAccount()); return tTest.getAccount(); } }@Controller public class TestMysqlController { @Autowired private TestService testService; @RequestMapping(value="/cardAddAcountMysql") @ResponseBody public TaotaoResult<Integer> cardAddAcount() throws TransactionalException{ TaotaoResult<Integer> result = new TaotaoResult<Integer>(); result.setData("+100, 余额: " + testService.addAcount("card", 100)); return result; } @RequestMapping(value="/passbookAddAcountMysql") @ResponseBody public TaotaoResult<Integer> passbookAddAcount() throws TransactionalException{ TaotaoResult<Integer> result = new TaotaoResult<Integer>(); result.setData("+100, 余额: " + testService.addAcount("存折", 100)); return result; } @RequestMapping(value="/cardSubAcountMysql") @ResponseBody public TaotaoResult<Integer> cardSubAcount(){ TaotaoResult<Integer> result = new TaotaoResult<Integer>(); result.setData("-150, 余额: " + testService.subAcount("card", 150)); return result; } @RequestMapping(value="/passbookSubAcountMysql") @ResponseBody public TaotaoResult<Integer> passbookSubAcount() throws TransactionalException{ TaotaoResult<Integer> result = new TaotaoResult<Integer>(); result.setData("-200, 余额: " + testService.subAcount("存折", 200)); return result; } @RequestMapping(value="/selectAcountMysql") @ResponseBody public TaotaoResult<Integer> selectAcount() throws TransactionalException { TaotaoResult<Integer> result = new TaotaoResult<Integer>(); result.setData(testService.selectAcount("")); return result; } }
2021年11月15日
62 阅读
0 评论
0 点赞
2021-11-10
Java中的微信支付(3):API V3对微信服务器响应进行签名验证
1. 前言 微信支付 V3 版本前两篇分别讲了如何对请求做签名和如何获取并刷新微信平台公钥,本篇将继续展开如何对微信支付响应结果的验签。2. 为什么要对响应验签 微信支付会在回调的 HTTP 头部中包括回调报文的签名。商户必须验证响应的签名,保证响应确实来自微信支付服务器,避免中间人攻击。而验证响应签名除了需要微信平台的公钥外还需要从请求头的其它参数。假设以下就是微信支付服务器的响应:HTTP/1.1 200 OK Server: nginx Date: Tue, 02 Apr 2019 12:59:40 GMT Content-Type: application/json; charset=utf-8 Content-Length: 2204 Connection: keep-alive Keep-Alive: timeout=8 Content-Language: zh-CN Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA== Wechatpay-Timestamp: 1554209980 Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1 Cache-Control: no-cache, must-revalidate {"prepay_id":"wx2922034726858082fbd40b511c67630000"}检查平台证书序列号微信支付响应的时候会携带一个微信平台证书序列号,从响应头中的Wechatpay-Serial字段中获取值,用来提示我们要使用该序列号的证书来进行验签,如果不存在就需要我们刷新证书,而上一文我们将平台证书序列号和证书以键值对存在HashMap中,我们只需要检查是否存在即可,不存在就刷新。构造验签名串从响应结果中获取对应下面方法的三个参数就可以构造出验签名串。/** * 构造验签名串. * * @param wechatpayTimestamp HTTP头 Wechatpay-Timestamp 中的应答时间戳。 * @param wechatpayNonce HTTP头 Wechatpay-Nonce 中的应答随机串 * @param body 响应体 * @return the string */ public String responseSign(String wechatpayTimestamp, String wechatpayNonce, String body) { return Stream.of(wechatpayTimestamp, wechatpayNonce, body) .collect(Collectors.joining("\n", "", "\n")); }验证签名待验证的签名从响应头中的Wechatpay-Signature字段中获取,我们使用微信支付平台公钥对验签名串和签名进行SHA256 with RSA签名验证。 // 构造验签名串 final String signatureStr = responseSign(wechatpayTimestamp, wechatpayNonce, body); // 加载SHA256withRSA签名器 Signature signer = Signature.getInstance("SHA256withRSA"); // 用微信平台公钥对签名器进行初始化 signer.initVerify(certificate); // 把我们构造的验签名串更新到签名器中 signer.update(signatureStr.getBytes(StandardCharsets.UTF_8)); // 把请求头中微信服务器返回的签名用Base64解码 并使用签名器进行验证 boolean result = signer.verify(Base64Utils.decodeFromString(wechatpaySignature));完整的验签代码/** * 我方对响应验签,和应答签名做比较,使用微信平台证书. * * @param wechatpaySerial response.headers['Wechatpay-Serial'] 当前使用的微信平台证书序列号 * @param wechatpaySignature response.headers['Wechatpay-Signature'] 微信平台签名 * @param wechatpayTimestamp response.headers['Wechatpay-Timestamp'] 微信服务器的时间戳 * @param wechatpayNonce response.headers['Wechatpay-Nonce'] 微信服务器提供的随机串 * @param body response.body 微信服务器的响应体 * @return the boolean */ @SneakyThrows public boolean responseSignVerify(String wechatpaySerial, String wechatpaySignature, String wechatpayTimestamp, String wechatpayNonce, String body) { if (CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) { refreshCertificate(); } Certificate certificate = CERTIFICATE_MAP.get(wechatpaySerial); final String signatureStr = createSign(wechatpayTimestamp, wechatpayNonce, body); Signature signer = Signature.getInstance("SHA256withRSA"); signer.initVerify(certificate); signer.update(signatureStr.getBytes(StandardCharsets.UTF_8)); return signer.verify(Base64Utils.decodeFromString(wechatpaySignature)); }CERTIFICATE_MAP 平台证书容器可参考上一篇文章。3. 总结 验签通过就说明我们请求的响应来自微信服务器就可以针对结果进行对应的逻辑处理了,微信支付 API 无论是 V2 还是 V3 都包含了使用Api 证书对请求进行加签,对响应结果进行验签的流程,十分考验对密码摘要算法的使用,其它无非就是组织参数调用 Http 请求。如果你能够掌握这一能力就会在面试中和工作中占到优势。
2021年11月10日
71 阅读
0 评论
0 点赞
2021-11-01
SpringBoot集成Shiro实现权限管理极简教程(一)
{card-describe title="前言"}Apache Shiro是一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。Shiro有三大核心组件:Subject : 即当前用户,在权限管理的应用程序里往往需要知道谁能够操作什么,谁拥有操作该程序的权利,shiro中则需要通过Subject来提供基础的当前用户信息,Subject 不仅仅代表某个用户,与当前应用交互的任何东西都是Subject,如网络爬虫等。所有的Subject都要绑定到SecurityManager上,与Subject的交互实际上是被转换为与SecurityManager的交互。SecurityManager : 即所有Subject的管理者,这是Shiro框架的核心组件,可以把他看做是一个Shiro框架的全局管理组件,用于调度各种Shiro框架的服务。作用类似于SpringMVC中的DispatcherServlet,用于拦截所有请求并进行处理。Realm : Realm是用户的信息认证器和用户的权限认证器,我们需要自己来实现Realm来自定义的管理我们自己系统内部的权限规则。SecurityManager要验证用户,需要从Realm中获取用户。可以把Realm看做是数据源。通过两篇文章,我们想用最简单的教程给大家实现,SpringBoot + Shiro +JWT 实现用户权限认证控制的演示。 {/card-describe}{mtitle title="一、数据库设计"/}1.1 用户表-User SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `account` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of user -- ---------------------------- INSERT INTO `user` VALUES (1, 'root', '超级用户', 'root'); INSERT INTO `user` VALUES (2, 'user', '普通用户', 'user'); INSERT INTO `user` VALUES (3, 'vip', 'VIP用户', 'vip'); SET FOREIGN_KEY_CHECKS = 1;{dotted startColor="#ff6c6c" endColor="#1989fa"/}1.2 角色表-RoleSET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for role -- ---------------------------- DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `desc` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of role -- ---------------------------- INSERT INTO `role` VALUES (1, 'admin', '超级管理员'); INSERT INTO `role` VALUES (2, 'user', '普通用户'); INSERT INTO `role` VALUES (3, 'vip_user', 'VIP用户'); SET FOREIGN_KEY_CHECKS = 1;{dotted startColor="#ff6c6c" endColor="#1989fa"/}1.3 权限表-PermissionSET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for permission -- ---------------------------- DROP TABLE IF EXISTS `permission`; CREATE TABLE `permission` ( `id` int(11) NOT NULL AUTO_INCREMENT, `permission` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限名称', `desc` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限描述', PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of permission -- ---------------------------- INSERT INTO `permission` VALUES (1, 'add', '增加'); INSERT INTO `permission` VALUES (2, 'update', '更新'); INSERT INTO `permission` VALUES (3, 'select', '查看'); INSERT INTO `permission` VALUES (4, 'delete', '删除'); SET FOREIGN_KEY_CHECKS = 1;{dotted startColor="#ff6c6c" endColor="#1989fa"/}1.4 用户角色表-User_RoleSET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for user_role -- ---------------------------- DROP TABLE IF EXISTS `user_role`; CREATE TABLE `user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NULL DEFAULT NULL, `role_id` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Fixed; -- ---------------------------- -- Records of user_role -- ---------------------------- INSERT INTO `user_role` VALUES (1, 1, 1); INSERT INTO `user_role` VALUES (2, 2, 2); INSERT INTO `user_role` VALUES (3, 3, 3); SET FOREIGN_KEY_CHECKS = 1;{dotted startColor="#ff6c6c" endColor="#1989fa"/}1.5 角色权限表-Role_PermissionSET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for role_permission -- ---------------------------- DROP TABLE IF EXISTS `role_permission`; CREATE TABLE `role_permission` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role_id` int(11) NULL DEFAULT NULL, `permission_id` int(255) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Fixed; -- ---------------------------- -- Records of role_permission -- ---------------------------- INSERT INTO `role_permission` VALUES (1, 1, 1); INSERT INTO `role_permission` VALUES (2, 1, 2); INSERT INTO `role_permission` VALUES (3, 1, 3); INSERT INTO `role_permission` VALUES (4, 1, 4); INSERT INTO `role_permission` VALUES (5, 2, 3); INSERT INTO `role_permission` VALUES (6, 3, 3); INSERT INTO `role_permission` VALUES (7, 3, 2); INSERT INTO `role_permission` VALUES (8, 2, 1); SET FOREIGN_KEY_CHECKS = 1;{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="二、项目准备"/}2.1 导入Pom<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>{dotted startColor="#ff6c6c" endColor="#1989fa"/}2.2 application.ymlserver: port: 8903 spring: application: name: lab-user datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/laboratory?charset=utf8 username: root password: root mybatis: type-aliases-package: cn.ntshare.laboratory.entity mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true{dotted startColor="#ff6c6c" endColor="#1989fa"/}2.3 实体类2.3.1 User.java@Data @ToString public class User implements Serializable { private static final long serialVersionUID = -6056125703075132981L; private Integer id; private String account; private String password; private String username; }2.3.2 Role.java@Data @ToString public class Role implements Serializable { private static final long serialVersionUID = -1767327914553823741L; private Integer id; private String role; private String desc; }{dotted startColor="#ff6c6c" endColor="#1989fa"/}2.4 Dao层2.4.1 PermissionMapper.java@Mapper @Repository public interface PermissionMapper { List<String> findByRoleId(@Param("roleIds") List<Integer> roleIds); }2.4.2 PermissionMapper.xml<mapper namespace="cn.ntshare.laboratory.dao.PermissionMapper"> <sql id="base_column_list"> id, permission, desc </sql> <select id="findByRoleId" parameterType="List" resultType="String"> select permission from permission, role_permission rp where rp.permission_id = permission.id and rp.role_id in <foreach collection="roleIds" item="id" open="(" close=")" separator=","> #{id} </foreach> </select> </mapper>2.4.3 RoleMapper.java@Mapper @Repository public interface RoleMapper { List<Role> findRoleByUserId(@Param("userId") Integer userId); }2.4.4 RoleMapper.xml<mapper namespace="cn.ntshare.laboratory.dao.RoleMapper"> <sql id="base_column_list"> id, user_id, role_id </sql> <select id="findRoleByUserId" parameterType="Integer" resultType="Role"> select role.id, role from role, user, user_role ur where role.id = ur.role_id and ur.user_id = user.id and user.id = #{userId} </select> </mapper>2.4.5 UserMapper.java@Mapper @Repository public interface UserMapper { User findByAccount(@Param("account") String account); }2.4.6 UserMapper.xml<mapper namespace="cn.ntshare.laboratory.dao.UserMapper"> <sql id="base_column_list"> id, account, password, username </sql> <select id="findByAccount" parameterType="Map" resultType="User"> select <include refid="base_column_list"/> from user where account = #{account} </select> </mapper>{dotted startColor="#ff6c6c" endColor="#1989fa"/}2.5 Service层2.5.1 PermissionServiceImpl.java@Service public class PermissionServiceImpl implements PermissionService { @Autowired private PermissionMapper permissionMapper; @Override public List<String> findByRoleId(List<Integer> roleIds) { return permissionMapper.findByRoleId(roleIds); } }2.5.2 RoleServiceImpl.java@Service public class RoleServiceImpl implements RoleService { @Autowired private RoleMapper roleMapper; @Override public List<Role> findRoleByUserId(Integer id) { return roleMapper.findRoleByUserId(id); } }2.5.3 UserServiceImpl.java@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public User findByAccount(String account) { return userMapper.findByAccount(account); } }{dotted startColor="#ff6c6c" endColor="#1989fa"/}2.6 系统返回状态枚举与包装函数2.6.1 ServerResponseEnum.java@AllArgsConstructor @Getter public enum ServerResponseEnum { SUCCESS(0, "成功"), ERROR(10, "失败"), ACCOUNT_NOT_EXIST(11, "账号不存在"), DUPLICATE_ACCOUNT(12, "账号重复"), ACCOUNT_IS_DISABLED(13, "账号被禁用"), INCORRECT_CREDENTIALS(14, "账号或密码错误"), NOT_LOGIN_IN(15, "账号未登录"), UNAUTHORIZED(16, "没有权限") ; Integer code; String message; }2.6.2 ServerResponseVO.java@Getter @Setter @NoArgsConstructor public class ServerResponseVO<T> implements Serializable { private static final long serialVersionUID = -1005863670741860901L; // 响应码 private Integer code; // 描述信息 private String message; // 响应内容 private T data; private ServerResponseVO(ServerResponseEnum responseCode) { this.code = responseCode.getCode(); this.message = responseCode.getMessage(); } private ServerResponseVO(ServerResponseEnum responseCode, T data) { this.code = responseCode.getCode(); this.message = responseCode.getMessage(); this.data = data; } private ServerResponseVO(Integer code, String message) { this.code = code; this.message = message; } /** * 返回成功信息 * @param data 信息内容 * @param <T> * @return */ public static<T> ServerResponseVO success(T data) { return new ServerResponseVO<>(ServerResponseEnum.SUCCESS, data); } /** * 返回成功信息 * @return */ public static ServerResponseVO success() { return new ServerResponseVO(ServerResponseEnum.SUCCESS); } /** * 返回错误信息 * @param responseCode 响应码 * @return */ public static ServerResponseVO error(ServerResponseEnum responseCode) { return new ServerResponseVO(responseCode); } }{dotted startColor="#ff6c6c" endColor="#1989fa"/}2.7 统一异常处理当用户身份认证失败时,会抛出UnauthorizedException,我们可以通过统一异常处理来处理该异常@RestControllerAdvice public class UserExceptionHandler { @ExceptionHandler(UnauthorizedException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ServerResponseVO UnAuthorizedExceptionHandler(UnauthorizedException e) { return ServerResponseVO.error(ServerResponseEnum.UNAUTHORIZED); } }{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="三、集成Shiro"/}3.1 UserRealm.java/** * 负责认证用户身份和对用户进行授权 */ public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private PermissionService permissionService; // 用户授权 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { User user = (User) principalCollection.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); List<Role> roleList = roleService.findRoleByUserId(user.getId()); Set<String> roleSet = new HashSet<>(); List<Integer> roleIds = new ArrayList<>(); for (Role role : roleList) { roleSet.add(role.getRole()); roleIds.add(role.getId()); } // 放入角色信息 authorizationInfo.setRoles(roleSet); // 放入权限信息 List<String> permissionList = permissionService.findByRoleId(roleIds); authorizationInfo.setStringPermissions(new HashSet<>(permissionList)); return authorizationInfo; } // 用户认证 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authToken; User user = userService.findByAccount(token.getUsername()); if (user == null) { return null; } return new SimpleAuthenticationInfo(user, user.getPassword(), getName()); } }3.2 ShiroConfig.java@Configuration public class ShiroConfig { @Bean public UserRealm userRealm() { return new UserRealm(); } @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm()); return securityManager; } /** * 路径过滤规则 * @return */ @Bean public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("/"); Map<String, String> map = new LinkedHashMap<>(); // 有先后顺序 map.put("/login", "anon"); // 允许匿名访问 map.put("/**", "authc"); // 进行身份认证后才能访问 shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; } /** * 开启Shiro注解模式,可以在Controller中的方法上添加注解 * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }3.3 LoginController.java@RestController @RequestMapping("") public class LoginController { @PostMapping("/login") public ServerResponseVO login(@RequestParam(value = "account") String account, @RequestParam(value = "password") String password) { Subject userSubject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(account, password); try { // 登录验证 userSubject.login(token); return ServerResponseVO.success(); } catch (UnknownAccountException e) { return ServerResponseVO.error(ServerResponseEnum.ACCOUNT_NOT_EXIST); } catch (DisabledAccountException e) { return ServerResponseVO.error(ServerResponseEnum.ACCOUNT_IS_DISABLED); } catch (IncorrectCredentialsException e) { return ServerResponseVO.error(ServerResponseEnum.INCORRECT_CREDENTIALS); } catch (Throwable e) { e.printStackTrace(); return ServerResponseVO.error(ServerResponseEnum.ERROR); } } @GetMapping("/login") public ServerResponseVO login() { return ServerResponseVO.error(ServerResponseEnum.NOT_LOGIN_IN); } @GetMapping("/auth") public String auth() { return "已成功登录"; } @GetMapping("/role") @RequiresRoles("vip") public String role() { return "测试Vip角色"; } @GetMapping("/permission") @RequiresPermissions(value = {"add", "update"}, logical = Logical.AND) public String permission() { return "测试Add和Update权限"; } } {dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="四、测试效果"/}4.1 用root用户登录4.1.1 登录4.1.2 验证是否登录4.1.3 测试角色权限4.1.4 测试用户操作权限{dotted startColor="#ff6c6c" endColor="#1989fa"/}4.2 user用户和vip用户测试略
2021年11月01日
60 阅读
0 评论
0 点赞
2021-10-27
Centos7 实现通过秘钥SSH登录
# 进入当前登陆了账号的用户目录 cd ~ # 生成秘钥 ssh-keygen # 一路回车 默认秘钥会生成在 当前用户根目录下的隐藏目录 .ssh 下面 # 查看秘钥, 一般情况下两个秘钥文件 一个公钥一个私钥 私钥: id_rsa 公钥: id_rsa.pub cd .ssh ll -a # 把远程用户的id_rsa.pub 文件内容直接复制进去, 每个用户的秘钥一行 vim authorized_keys # 编辑sshd配置文件 vim /etc/ssh/sshd_config #开启pubkey登录 PubkeyAuthentication yes # 重新启动一下服务 systemctl restart sshd.service # 然后远程用户电脑测试连接当前服务器 ssh username@ip:port # 能连上代表成功了
2021年10月27日
51 阅读
0 评论
0 点赞
2021-10-27
java8时间日期工具类,整个项目有它就足够了
{card-describe title="前言"} 在JDK8之前日期时间工具类都在java.util中,有Date、Calendar、TimeZone等,虽然是在util工具包下,但是却很不完善,使用者总是要封装各种时间操作工具类,并且缺乏年月日星期等的加减操作,而且还不是线程安全的,在JDK8之后,java设计者终于引入了新的日期时间库,这些类库都在java.time包下,对日期时间的操作非常方便,参考网上然后自己总结的一个日期时间工具类,整个项目有这个时间工具类就足够了,欢迎收藏备用。{/card-describe}工具类代码 import java.sql.Timestamp; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; public class Java8DatetimeUtil { /** * 日期格式 */ private static DateTimeFormatter date_formatter_default = DateTimeFormatter.ofPattern("yyyy-MM-dd"); /** * 日期时间格式 */ private static DateTimeFormatter datetime_formatter_default = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); /** * 返回给定日期的所在季度的最后一天 * * @param date 某个日期 * @return 某季度的最后一天 */ public static LocalDate lastDayOfQuarter(LocalDate date) { return LocalDate.of(date.getYear(), date.getMonth().firstMonthOfQuarter().plus(2), 1) .with(TemporalAdjusters.lastDayOfMonth()); } /** * 返回给定日期的所在季度的第一天 * * @param date 某个日期 * @return 某季度的第一天 */ public static LocalDate firstDayOfQuarter(LocalDate date) { return LocalDate.of(date.getYear(), date.getMonth().firstMonthOfQuarter(), 1); } /** * 返回给定日期的所在月的最后一天 * * @param date 某个日期 * @return 月的最后一天 */ public static LocalDate lastDayOfMonth(LocalDate date) { return date.with(TemporalAdjusters.lastDayOfMonth()); } /** * 返回给定日期的所在月的第一天 * * @param date 某个日期 * @return 月的第一天 */ public static LocalDate firstDayOfMonth(LocalDate date) { return LocalDate.of(date.getYear(), date.getMonth(), 1); } /** * 比较两个日期是否相等 * * @param sourceDate 需要比较的日期 * @param targetDate 比较的日期 * @return */ public static boolean equals(LocalDate sourceDate, LocalDate targetDate) { return sourceDate.equals(targetDate); } /** * 字符串日期转为LocalDate * * @param date 需要转换的字符串日期 * @return */ public static LocalDate parseDate(String date) { return LocalDate.parse(date, date_formatter_default); } /** * 字符串时间转为LocalDateTime * * @param date 需要转换的字符串时间 * @return */ public static LocalDateTime parseDatetime(String date) { return LocalDateTime.parse(date, datetime_formatter_default); } /** * 返回当前日期字符串 * * @return */ public static String today() { return LocalDate.now().format(date_formatter_default); } /** * 返回给定格式的当前日期字符串 * * @return */ public static String today(String pattern) { DateTimeFormatter _pattern = DateTimeFormatter.ofPattern(pattern); return LocalDate.now().format(_pattern); } /** * 返回当前日期时间字符串 * * @return */ public static String now() { return LocalDateTime.now().format(datetime_formatter_default); } /** * 返回给定格式的当前时间字符串 * * @return */ public static String now(String pattern) { DateTimeFormatter _pattern = DateTimeFormatter.ofPattern(pattern); return LocalDateTime.now().format(_pattern); } /** * 格式化日期为相应的字符串 * * @param date * @return */ public static String formatDate(LocalDate date) { return date.format(date_formatter_default); } /** * 格式化日期为相应的字符串 * * @param date * @param pattern * @return */ public static String format(LocalDate date, String pattern) { DateTimeFormatter _pattern = DateTimeFormatter.ofPattern(pattern); return date.format(_pattern); } /** * 格式化时间为相应的字符串 * * @param datetime * @return */ public static String formatDatetime(LocalDateTime datetime) { return datetime.format(datetime_formatter_default); } /** * 格式化时间为相应的字符串 * * @param datetime * @param pattern * @return */ public static String format(LocalDateTime datetime, String pattern) { DateTimeFormatter _pattern = DateTimeFormatter.ofPattern(pattern); return datetime.format(_pattern); } /** * 时间戳转日期 * * @param timestamp * @return */ public static LocalDate long2Date(long timestamp) { //ZoneId.systemDefault() ZoneId shanghaiZone = ZoneId.of("UTC+8"); return Instant.ofEpochMilli(timestamp).atZone(shanghaiZone).toLocalDate(); } /** * 时间戳转时间 * * @param timestamp * @return */ public static LocalDateTime long2DateTime(long timestamp) { //ZoneId.systemDefault() ZoneId shanghaiZone = ZoneId.of("UTC+8"); return Instant.ofEpochMilli(timestamp).atZone(shanghaiZone).toLocalDateTime(); } /** * 日期转时间戳 * * @param date * @return */ public static long date2Long(LocalDate date) { return date.atStartOfDay(ZoneId.of("UTC+8")).toInstant().toEpochMilli(); // return Timestamp.valueOf(date.atStartOfDay()).getTime(); } /** * 时间转时间戳 * * @param dateTime * @return */ public static long dateTime2Long(LocalDateTime dateTime) { return dateTime.atZone(ZoneId.of("UTC+8")).toInstant().toEpochMilli(); // return Timestamp.valueOf(dateTime).getTime(); } /** * 时间字符串转时间戳 * * @param dateTime * @return */ public static long dateTime2Long(String dateTime) { return Timestamp.valueOf(dateTime).getTime(); } /** * 日期的加减在java8内置方法已经非常使用,可以不用调这个接口的。 * // 当天日期前1天:localDate.minusDays(1); * // 当天日期减1个月:localDate.minusMonths(1); * // 当前日期后2天:localDate.plusDays(2); * // 当前日期加1个月:localDate.plusMonths(1); * // 当前日期加1周:localDate.plusWeeks(1); * // 当前日期加1年:localDate.plusYears(1); * <p> * // 当天时间前1天:localDateTime.minusDays(1); * // 当天时间减1个月:localDateTime.minusMonths(1); * // 当前时间后2天:localDateTime.plusDays(2); * // 当前时间加1个月:localDateTime.plusMonths(1); * // 当前时间加1周:localDateTime.plusWeeks(1); * // 当前时间加1年:localDateTime.plusYears(1); * <br>建议直接使用原始方法操作<br> * * <p>日期的加减</p> * 如获取当前日期的前30天的日期, plus(LocalDate.now(),-30,ChronoUnit.DAYS) * * @param localDate 当前日期 * @param between 天数,可以数负数(等于减) * @param chronoUnit 单位,天、周、月、年等 * @return */ public static LocalDate plus(LocalDate localDate, int between, ChronoUnit chronoUnit) { return localDate.plus(between, chronoUnit); } } 代码演示:System.out.println("2019-11-03=2019-11-04吗?答案是:" + Java8DatetimeUtil .equals(Java8DatetimeUtil.parseDate("2019-11-03"), Java8DatetimeUtil.parseDate("2019-11-04"))); System.out.println("当天日期字符串:" + Java8DatetimeUtil.today()); System.out.println("当天时间字符串:" + Java8DatetimeUtil.now()); System.out.println("当天日期转换为时间戳:" + Java8DatetimeUtil.date2Long(LocalDate.now())); System.out.println("当天时间转换为时间戳:" + Java8DatetimeUtil.dateTime2Long(LocalDateTime.now())); System.out.println("字符串2019-11-03 11:20:33转换为时间戳:" + Java8DatetimeUtil.dateTime2Long("2019-11-03 11:20:33")); System.out.println("1635177213756转为日期:" + Java8DatetimeUtil.long2Date(1635177213756L)); System.out.println("1635177213756转为时间:" + Java8DatetimeUtil.long2DateTime(1635177213756L)); System.out.println("当天日期转为字符串:" + Java8DatetimeUtil.formatDate(LocalDate.now())); System.out.println("当天日期转为字符串(指定格式):" + Java8DatetimeUtil.format(LocalDate.now(), "yyyy/MM/dd")); System.out.println("当天时间转为字符串:" + Java8DatetimeUtil.formatDatetime(LocalDateTime.now())); System.out.println("当天时间转为字符串(指定格式):" + Java8DatetimeUtil.format(LocalDateTime.now(), "yyyy/MM/dd HH.mm.ss")); System.out.println("字符串2019-11-03转换为LocalDate:" + Java8DatetimeUtil.parseDate("2019-11-03")); System.out.println( "字符串2019-11-03 11:20:33转换为LocalDateTime:" + Java8DatetimeUtil.parseDatetime("2019-11-03 11:20:33")); System.out.println("当天所在季度第一天:" + Java8DatetimeUtil.firstDayOfQuarter(LocalDate.now())); System.out.println( "指定日期所在季度最后一天:" + Java8DatetimeUtil.lastDayOfQuarter(Java8DatetimeUtil.parseDate("2019-11-03"))); System.out.println("当天所在月的最后一天:" + Java8DatetimeUtil.lastDayOfMonth(LocalDate.now())); System.out.println("当天所在月的第一天:" + Java8DatetimeUtil.firstDayOfMonth(LocalDate.now())); System.out.println("2019-11-03减2天:" + Java8DatetimeUtil.parseDate("2019-11-03").minusDays(2)); System.out.println("2019-11-03加2天:" + Java8DatetimeUtil.parseDate("2019-11-03").plusDays(2));
2021年10月27日
175 阅读
0 评论
0 点赞
2021-10-26
XStream生成XMl文件,设置别名
{card-describe title="前言"} 一般来说通过现在给接口发送数据都用JSON格式,但是给例如银行这些平台进行数据传输的时候还可能使用XML格式的报文,XML格式的报文通常会包含HEAD、BODY等节点数据,我们可以将业务数据通过XStream进行处理,可以以下面的代码进行参考:{/card-describe}{dotted startColor="#ff6c6c" endColor="#1989fa"/}public class B2BPayToXml { public Head head; public Body body; public void setHead(Head head) { this.head = head; } public void setBody(Body body) { this.body = body; } }/** * 报文头部信息 * @author lizhiyong * @version $Id: Head.java, v 0.1 2014年9月24日 上午10:01:57 Exp $ */ public class Head { public String MerPtcId; public String TranTime; public String TranCode; public String TranDate; public Head(String merPtcId, String tranTime, String tranCode, String tranDate) { MerPtcId = merPtcId; TranTime = tranTime; TranCode = tranCode; TranDate = tranDate; } }/** * 报文体信息 * @author lizhiyong * @version $Id: Body.java, v 0.1 2014年9月24日 上午10:03:30 Exp $ */ public class Body { public String MerTranSerialNo; public String SafeReserved; //协议信息 public PtcInfo ptcInfo; //业务信息 public BusiInfo busiInfo; //会员信息 public UserInfo userInfo; //商品信息 public GoodsInfo goodsInfo; //交易信息 public TranInfo tranInfo; //通道信息 public ChannelInfo channelInfo; //备注信息 public MemoInfo memoInfo; public void setMerTranSerialNo(String merTranSerialNo) { MerTranSerialNo = merTranSerialNo; } public void setSafeReserved(String safeReserved) { SafeReserved = safeReserved; } public void setPtcInfo(PtcInfo ptcInfo) { this.ptcInfo = ptcInfo; } public void setBusiInfo(BusiInfo busiInfo) { this.busiInfo = busiInfo; } public void setUserInfo(UserInfo userInfo) { this.userInfo = userInfo; } public void setGoodsInfo(GoodsInfo goodsInfo) { this.goodsInfo = goodsInfo; } public void setTranInfo(TranInfo tranInfo) { this.tranInfo = tranInfo; } public void setChannelInfo(ChannelInfo channelInfo) { this.channelInfo = channelInfo; } public void setMemoInfo(MemoInfo memoInfo) { this.memoInfo = memoInfo; } }/** * 会员信息 * @author lizhiyong * @version $Id: UserInfo.java, v 0.1 2014年9月24日 上午10:08:47 Exp $ */ public class UserInfo { public String BuyerId; public String BuyerName; public String SellerId; public String SellerName; public UserInfo(String buyerId, String buyerName, String sellerId, String sellerName) { BuyerId = buyerId; BuyerName = buyerName; SellerId = sellerId; SellerName = sellerName; } }XStream xStream = new XStream(new DomDriver()); xStream.alias("Document", B2BPayToXml.class); //设置类别名 xStream.aliasField("Head", B2BPayToXml.class, "head"); xStream.aliasField("Body", B2BPayToXml.class, "body"); xStream.aliasField("PtcInfo", Body.class, "ptcInfo"); xStream.aliasField("BusiInfo", Body.class, "busiInfo"); xStream.aliasField("UserInfo", Body.class, "userInfo"); xStream.aliasField("GoodsInfo", Body.class, "goodsInfo"); xStream.aliasField("TranInfo", Body.class, "tranInfo"); xStream.aliasField("ChannelInfo", Body.class, "channelInfo"); xStream.aliasField("MemoInfo", Body.class, "memoInfo"); B2BPayToXml bToXml = new B2BPayToXml(); //头部信息 bToXml.setHead(new Head(merPtcId, tranTime, tranCode, tranDate)); //协议信息 PtcInfo ptcInfo = new PtcInfo(subMerPtcId); //业务信息 BusiInfo busiInfo = new BusiInfo(merOrderNo); //会员信息 UserInfo userInfo = new UserInfo(buyerId, buyerName, sellerId, sellerName); //商品信息 GoodsInfo goodsInfo = new GoodsInfo(goodsName, goodsTxt, goodsDesc); //交易信息 TranInfo tranInfo = new TranInfo(tranModeId, tranAmt, tranCry); //通道信息 ChannelInfo channelInfo = new ChannelInfo(channelApi, channelInst); //备注信息 MemoInfo memoInfo = new MemoInfo(buyerMemo, sellerMemo, platMemo, payMemo); Body body = new Body(); body.setMerTranSerialNo(merTranSerialNo); body.setSafeReserved(safeReserved); body.setPtcInfo(ptcInfo); body.setBusiInfo(busiInfo); body.setUserInfo(userInfo); body.setGoodsInfo(goodsInfo); body.setTranInfo(tranInfo); body.setChannelInfo(channelInfo); body.setMemoInfo(memoInfo); bToXml.setBody(body); String top = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \n"; String xml = top + xStream.toXML(bToXml);<?xml version="1.0" encoding="UTF-8"?> <Document> <Head> <MerPtcId>0000</MerPtcId> <TranTime>0000</TranTime> <TranCode>0000</TranCode> <TranDate>0000</TranDate> </Head> <Body> <MerTranSerialNo>0000</MerTranSerialNo> <SafeReserved>0000</SafeReserved> <PtcInfo> <SubMerPtcId>0000</SubMerPtcId> </PtcInfo> <BusiInfo> <MerOrderNo>0000</MerOrderNo> </BusiInfo> <UserInfo> <BuyerId>0000</BuyerId> <BuyerName>0000</BuyerName> <SellerId>0000</SellerId> <SellerName>0000</SellerName> </UserInfo> <GoodsInfo> <GoodsName>0000</GoodsName> <GoodsTxt>0000</GoodsTxt> <GoodsDesc>0000</GoodsDesc> </GoodsInfo> <TranInfo> <TranModeId>0000</TranModeId> <TranAmt>0000</TranAmt> <TranCry>0000</TranCry> </TranInfo> <ChannelInfo> <ChannelApi>0000</ChannelApi> <ChannelInst>0000</ChannelInst> </ChannelInfo> <MemoInfo> <BuyerMemo>0000</BuyerMemo> <SellerMemo>0000</SellerMemo> <PlatMemo>0000</PlatMemo> <PayMemo>0000</PayMemo> </MemoInfo> </Body> </Document>
2021年10月26日
89 阅读
0 评论
0 点赞
2021-10-26
SpringBoot应用中使用AOP实现自定义登录注解及登录信息注入
{card-describe title="前言"}HandlerInterceptor经常被用来解决拦截事件,如用户鉴权等。另外,Spring也向我们提供了多种解析器Resolver,如用来统一处理异常的HandlerExceptionResolver,以及今天的主角HandlerMethodArgumentResolver。HandlerMethodArgumentResolver是用来处理方法参数的解析器,包含以下2个方法:supportsParameter(满足某种要求,返回true,方可进入resolveArgument做参数处理)resolveArgument{/card-describe}{dotted startColor="#ff6c6c" endColor="#1989fa"/}第一步:首先我们需要在Shiro里面添加需要放开的接口第二步:创建@Login的自定义注解/** * app登录效验 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Login { }第三步:创建@LoginUser的自定义注解/** * 登录用户信息 */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface LoginUser { } 第四步:利用HandlerInterceptorAdapter这个适配器来实现自己的拦截器 主要作用是实现带有@Login注解的Controller强制要求登录,验证登陆后将当前用户数据注入到到request里,方便后续根据userId,获取用户信息。/** * 权限(Token)验证 */ @Component public class AuthorizationInterceptor extends HandlerInterceptorAdapter { @Autowired private JwtUtils jwtUtils; public static final String USER_KEY = "userId"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Login annotation; if(handler instanceof HandlerMethod) { annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class); }else{ return true; } if(annotation == null){ return true; } //获取用户凭证 String token = request.getHeader(jwtUtils.getHeader()); if(StringUtils.isBlank(token)){ token = request.getParameter(jwtUtils.getHeader()); } //凭证为空 if(StringUtils.isBlank(token)){ throw new BdAdminException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value()); } Claims claims = jwtUtils.getClaimByToken(token); if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){ throw new BdAdminException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value()); } //设置userId到request里,后续根据userId,获取用户信息 request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject())); return true; } }第五步:自定义方法参数解析器实现有@LoginUser注解的方法参数,注入当前登录用户@Component public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { @Autowired private UserService userService; @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterType().isAssignableFrom(UserEntity.class) && parameter.hasParameterAnnotation(LoginUser.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest request, WebDataBinderFactory factory) throws Exception { //获取用户ID Object object = request.getAttribute(AuthorizationInterceptor.USER_KEY, RequestAttributes.SCOPE_REQUEST); if(object == null){ return null; } //获取用户信息 UserEntity user = userService.getById((Long)object); return user; } }第六步:通过WebMvcConfig注册我们添加的拦截器、方法参数解析器/** * MVC配置 */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private AuthorizationInterceptor authorizationInterceptor; @Autowired private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authorizationInterceptor).addPathPatterns("/app/**"); } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(loginUserHandlerMethodArgumentResolver); } } 第七步:具体示例/** * APP测试接口 */ @RestController @RequestMapping("/app") @Api("APP测试接口") public class AppTestController { @Login @GetMapping("userInfo") @ApiOperation("获取用户信息") public R userInfo(@LoginUser UserEntity user){ return R.ok().put("user", user); } @Login @GetMapping("userId") @ApiOperation("获取用户ID") public R userInfo(@RequestAttribute("userId") Integer userId){ return R.ok().put("userId", userId); } @GetMapping("notToken") @ApiOperation("忽略Token验证测试") public R notToken(){ return R.ok().put("msg", "无需token也能访问。。。"); } }
2021年10月26日
149 阅读
0 评论
0 点赞
2021-10-26
SpringBoot应用中使用AOP记录接口操作日志
第一步:在annotation包下创建一个@interface类型的Log/** * 自定义操作日志记录注解 * */ @Target({ ElementType.PARAMETER, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Log { /** * 模块 */ public String title() default ""; /** * 功能 */ public BusinessType businessType() default BusinessType.OTHER; /** * 操作人类别 */ public OperatorType operatorType() default OperatorType.MANAGE; /** * 是否保存请求的参数 */ public boolean isSaveRequestData() default true; /** * 是否保存响应的参数 */ public boolean isSaveResponseData() default true; } 第二步:在aspectj包下面建立一个LogAspect/** * 操作日志记录处理 */ @Aspect @Component public class LogAspect { private static final Logger LOG = LoggerFactory.getLogger(LogAspect.class); private static final String LOG_IGNORE_PROPERTIES = "createBy,createDate,updateBy,updateDate,version,updateIp,delFlag,remoteAddr,requestUri,method,userAgent"; // 配置织入点 @Pointcut("@annotation(com.aidex.common.annotation.Log)") public void logPointCut() { } @Around("@annotation(com.aidex.common.annotation.Log)") public Object loggingAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); // 定义返回对象、得到方法需要的参数 Object resultData = null; Object[] args = joinPoint.getArgs(); long takeUpTime = 0; resultData = joinPoint.proceed(args); long endTime = System.currentTimeMillis(); takeUpTime = endTime - startTime; handleLog(joinPoint, null, resultData, takeUpTime); return resultData; } /** * 拦截异常操作 * * @param joinPoint 切点 * @param e 异常 */ @AfterThrowing(value = "logPointCut()", throwing = "e") public void doAfterThrowing(JoinPoint joinPoint, Exception e) { handleLog(joinPoint, e, null, 0); } protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, long takeUpTime) { try { // 获得注解 Log controllerLog = getAnnotationLog(joinPoint); if (controllerLog == null) { return; } // 获取当前的用户 LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest()); // *========数据库日志=========*// SysOperLog operLog = new SysOperLog(); operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); // 请求的地址 String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); operLog.setOperIp(ip); operLog.setOperUrl(ServletUtils.getRequest().getRequestURI()); if (loginUser != null) { operLog.setOperName(loginUser.getUsername()); } if (e != null) { operLog.setStatus(BusinessStatus.FAIL.ordinal()); operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); } // 设置方法名称 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); operLog.setMethod(className + "." + methodName + "()"); // 设置请求方式 operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); // 处理设置注解上的参数 getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); operLog.setTakeUpTime(takeUpTime); //设置数据变更 setLogContent(operLog); // 保存数据库 AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); } catch (Exception exp) { // 记录本地异常日志 LOG.error("==前置通知异常=="); LOG.error("异常信息:{}", exp.getMessage()); exp.printStackTrace(); } } private void setLogContent(SysOperLog operLog) { if (ContextHandler.get(BaseEntity.LOG_TYPE) != null) { String logType = (String) ContextHandler.get(BaseEntity.LOG_TYPE); if (StringUtils.isNotBlank(logType)) { if ("insert".equals(logType)) { // 异步保存日志 } else if ("update".equals(logType)) { } else if ("delete".equals(logType)) { } } } } /** * 获取注解中对方法的描述信息 用于Controller层注解 * * @param log 日志 * @param operLog 操作日志 * @throws Exception */ public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception { // 设置action动作 operLog.setBusinessType(log.businessType().ordinal()); // 设置标题 operLog.setTitle(log.title()); // 设置操作人类别 operLog.setOperatorType(log.operatorType().ordinal()); // 是否需要保存request,参数和值 if (log.isSaveRequestData()) { // 获取参数的信息,传入到数据库中。 setRequestValue(joinPoint, operLog); } // 是否需要保存response,参数和值 if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) { operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); } } /** * 获取请求的参数,放到log中 * * @param operLog 操作日志 * @throws Exception 异常 */ private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception { String requestMethod = operLog.getRequestMethod(); if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) { String params = argsArrayToString(joinPoint.getArgs()); operLog.setOperParam(StringUtils.substring(params, 0, 2000)); } else { Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000)); } } /** * 是否存在注解,如果存在就获取 */ private Log getAnnotationLog(JoinPoint joinPoint) throws Exception { Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); if (method != null) { return method.getAnnotation(Log.class); } return null; } /** * 参数拼装 */ private String argsArrayToString(Object[] paramsArray) { String params = ""; if (paramsArray != null && paramsArray.length > 0) { for (Object o : paramsArray) { if (StringUtils.isNotNull(o) && !isFilterObject(o)) { try { Object jsonObj = JSON.toJSON(o); params += jsonObj.toString() + " "; } catch (Exception e) { } } } } return params.trim(); } /** * 判断是否需要过滤的对象。 * * @param o 对象信息。 * @return 如果是需要过滤的对象,则返回true;否则返回false。 */ @SuppressWarnings("rawtypes") public boolean isFilterObject(final Object o) { Class<?> clazz = o.getClass(); if (clazz.isArray()) { return clazz.getComponentType().isAssignableFrom(MultipartFile.class); } else if (Collection.class.isAssignableFrom(clazz)) { Collection collection = (Collection) o; for (Object value : collection) { return value instanceof MultipartFile; } } else if (Map.class.isAssignableFrom(clazz)) { Map map = (Map) o; for (Object value : map.entrySet()) { Map.Entry entry = (Map.Entry) value; return entry.getValue() instanceof MultipartFile; } } return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse || o instanceof BindingResult; } }第三步:在Controller中使用自定义的注解记录操作@Log(title = "用户管理", businessType = BusinessType.EXPORT)
2021年10月26日
116 阅读
0 评论
0 点赞
2021-10-22
SpringBoot 如何进行限流?老鸟们都这么玩的!
今天来聊聊在SpringBoot项目中如何对接口进行限流,有哪些常见的限流算法,如何优雅的进行限流。为什么要进行限流?因为互联网系统通常都要面对大并发大流量的请求,在突发情况下(最常见的场景就是秒杀、抢购),瞬时大流量会直接将系统打垮,无法对外提供服务。那为了防止出现这种情况最常见的解决方案之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。例如,12306购票系统,在面对高并发的情况下,就是采用了限流。在流量高峰期间经常会出现提示语;"当前排队人数较多,请稍后再试!"什么是限流?有哪些限流算法?限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。{dotted startColor="#ff6c6c" endColor="#1989fa"/}常见的限流算法有三种:1.计数器限流计数器限流算法是最为简单粗暴的解决方案,主要用来限制总并发数,比如数据库连接池大小、线程池大小、接口访问并发数等都是使用计数器算法。如:使用 AomicInteger 来进行统计当前正在并发执行的次数,如果超过域值就直接拒绝请求,提示系统繁忙。2.漏桶算法漏桶算法思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。3.令牌桶算法令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。基于Guava工具类实现限流Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法实现流量限制,使用十分方便,而且十分高效,实现步骤如下:第一步:引入guava依赖包<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1-jre</version> </dependency>第二步:给接口加上限流逻辑@Slf4j @RestController @RequestMapping("/limit") public class LimitController { /** * 限流策略 :1秒钟2个请求 */ private final RateLimiter limiter = RateLimiter.create(2.0); private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @GetMapping("/test1") public String testLimiter() { //500毫秒内,没拿到令牌,就直接进入服务降级 boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS); if (!tryAcquire) { log.warn("进入服务降级,时间{}", LocalDateTime.now().format(dtf)); return "当前排队人数较多,请稍后再试!"; } log.info("获取令牌成功,时间{}", LocalDateTime.now().format(dtf)); return "请求成功"; } }以上用到了RateLimiter的2个核心方法:create()、tryAcquire(),以下为详细说明acquire() 获取一个令牌, 改方法会阻塞直到获取到这一个令牌, 返回值为获取到这个令牌花费的时间acquire(int permits) 获取指定数量的令牌, 该方法也会阻塞, 返回值为获取到这 N 个令牌花费的时间tryAcquire() 判断时候能获取到令牌, 如果不能获取立即返回 falsetryAcquire(int permits) 获取指定数量的令牌, 如果不能获取立即返回 falsetryAcquire(long timeout, TimeUnit unit) 判断能否在指定时间内获取到令牌, 如果不能获取立即返回 falsetryAcquire(int permits, long timeout, TimeUnit unit) 同上第三步:体验效果通过访问测试地址:http://127.0.0.1:8080/limit/test1,反复刷新并观察后端日志WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37 WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37 INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:37 WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37 WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37 INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:37 WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:38 INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:38 WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:38 INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:38从以上日志可以看出,1秒钟内只有2次成功,其他都失败降级了,说明我们已经成功给接口加上了限流功能。当然了,我们在实际开发中并不能直接这样用。至于原因嘛,你想呀,你每个接口都需要手动给其加上tryAcquire(),业务代码和限流代码混在一起,而且明显违背了DRY原则,代码冗余,重复劳动。代码评审时肯定会被老鸟们给嘲笑一番,啥破玩意儿!{dotted startColor="#ff6c6c" endColor="#1989fa"/}所以,我们这里需要想办法将其优化 - 借助自定义注解+AOP实现接口限流。基于AOP实现接口限流基于AOP的实现方式也非常简单,实现过程如下:第一步:加入AOP依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>第二步:自定义限流注解@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface Limit { /** * 资源的key,唯一 * 作用:不同的接口,不同的流量控制 */ String key() default ""; /** * 最多的访问限制次数 */ double permitsPerSecond () ; /** * 获取令牌最大等待时间 */ long timeout(); /** * 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒 */ TimeUnit timeunit() default TimeUnit.MILLISECONDS; /** * 得不到令牌的提示语 */ String msg() default "系统繁忙,请稍后再试."; }第三步:使用AOP切面拦截限流注解@Slf4j @Aspect @Component public class LimitAop { /** * 不同的接口,不同的流量控制 * map的key为 Limiter.key */ private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap(); @Around("@annotation(com.jianzh5.blog.limit.Limit)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable{ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //拿limit的注解 Limit limit = method.getAnnotation(Limit.class); if (limit != null) { //key作用:不同的接口,不同的流量控制 String key=limit.key(); RateLimiter rateLimiter = null; //验证缓存是否有命中key if (!limitMap.containsKey(key)) { // 创建令牌桶 rateLimiter = RateLimiter.create(limit.permitsPerSecond()); limitMap.put(key, rateLimiter); log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond()); } rateLimiter = limitMap.get(key); // 拿令牌 boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit()); // 拿不到命令,直接返回异常提示 if (!acquire) { log.debug("令牌桶={},获取令牌失败",key); this.responseFail(limit.msg()); return null; } } return joinPoint.proceed(); } /** * 直接向前端抛出异常 * @param msg 提示信息 */ private void responseFail(String msg) { HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); ResultData<Object> resultData = ResultData.fail(ReturnCode.LIMIT_ERROR.getCode(), msg); WebUtils.writeJson(response,resultData); } }第四步:给需要限流的接口加上注解@Slf4j @RestController @RequestMapping("/limit") public class LimitController { @GetMapping("/test2") @Limit(key = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!") public String limit2() { log.info("令牌桶limit2获取令牌成功"); return "ok"; } @GetMapping("/test3") @Limit(key = "limit3", permitsPerSecond = 2, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "系统繁忙,请稍后再试!") public String limit3() { log.info("令牌桶limit3获取令牌成功"); return "ok"; } }第五步:体验效果通过访问测试地址:http://127.0.0.1:8080/limit/test2,反复刷新并观察输出结果:正常响应时:{"status":100,"message":"操作成功","data":"ok","timestamp":1632579377104}触发限流时:{"status":2001,"message":"系统繁忙,请稍后再试!","data":null,"timestamp":1632579332177}通过观察得之,基于自定义注解同样实现了接口限流的效果。小结一般在系统上线时我们通过对系统压测可以评估出系统的性能阀值,然后给接口加上合理的限流参数,防止出现大流量请求时直接压垮系统。今天我们介绍了几种常见的限流算法(重点关注令牌桶算法),基于Guava工具类实现了接口限流并利用AOP完成了对限流代码的优化。在完成优化后业务代码和限流代码解耦,开发人员只要一个注解,不用关心限流的实现逻辑,而且减少了代码冗余大大提高了代码可读性,代码评审时谁还敢再笑话你?
2021年10月22日
77 阅读
0 评论
0 点赞
2021-10-21
JAVA&JS 针对数组(集合)去重
一、针对JS{callout color="#f0ad4e"}判断数组中是否包含对象,有的话不加入到数组中{/callout}// 判断数组中是否包含对象 export const isHasObj = (arr, item) => { let flag = false// true为有 false为没有 for (var i = 0; i < arr.length; i++) { if (JSON.stringify(arr[i]).indexOf(JSON.stringify(item)) !== -1) { flag = true } } return flag }二、针对JAVAlist.stream().distinct().collect(Collectors.toList())
2021年10月21日
18 阅读
0 评论
0 点赞
2021-10-14
三个场景,让你了解JAVA多线程
Java多线程是考量一个Java中级研发工程师的重要指标之一,小编通过几个典型的场景,以故事的形式,将Java多线程中的要点呈现给各位看客。Java多线程主要涉及到的编程技术有以下五点:对同一个变量进行操作对同一个对象进行操作回调方法使用线程同步,死锁问题线程通信场景一:电影院门口场景二:银行里的钱两个人AB,使用一个账户,A在柜台取钱和B在ATM机取钱程序分析:钱的数量要设置成一个静态的变量。两个人要取的同一个对象值故事三:龟兔赛跑龟兔赛跑:20米 //只要为了看到效果,所有距离缩短了要求:1.兔子每秒3米的速度,每跑6米休息10秒,2.乌龟每秒跑1米,不休息3.其中一个跑到终点后另一个不跑了!程序设计思路:1.创建一个Animal动物类,继承Thread,编写一个running抽象方法,重写run方法,把running方法在run方法里面调用。2.创建Rabbit兔子类和Tortoise乌龟类,继承动物类3.两个子类重写running方法4.本题的第3个要求涉及到线程回调。需要在动物类创建一个回调接口,创建一个回调对象
2021年10月14日
37 阅读
0 评论
0 点赞
2021-09-13
分布式锁Redisson,完美解决高并发问题
我们的活动遇到了性能问题,原先的单机锁性能太差,以下单购买商品为例,我们考虑使用分布式锁,但传统的方式为了使用Redis锁,我们需要设置一个定长的key,然后当购买完成后,将key删除。但为了防止key提前过期,我们不得不新增一个线程执行定时任务。现在我们可以使用Redissson框架简化代码。getLock()方法代替了Redis的setIfAbsent(),lock()设置过期时间。最终我们在交易结束后释放锁。延长锁的操作则有Redisson框架替我们完成,它会使用轮询去查看key是否过期,在交易没有完成时,自动重设Redis的key过期时间。1.引入依赖:<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.11.5</version> </dependency>2.配置redissonimport org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * redisson配置 * 目前使用的是腾讯云的单节点redis,因此暂时配置单服务 * * */ @Configuration public class RedissonConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private String port; @Value("${spring.redis.password}") private String password; @Bean public RedissonClient getRedisson(){ Config config = new Config(); config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password); //添加主从配置 //config.useMasterSlaveServers().setMasterAddress("").setPassword("").addSlaveAddress(new String[]{"",""}); return Redisson.create(config); } }3.示例代码import org.redisson.api.RedissonClient; import org.redisson.api.RLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BuyRedissonLock { @Autowired private RedissonClient redissonClient; @GetMapping(value = "buy") public String get() { RLock ztLock = redissonClient.getLock("ztLock"); // ztLock.lock(3, TimeUnit.SECONDS); 指定了超时时间的话,使用指定的, ztLock.lock(); //没指定的话使用30s,有看门狗,延迟10秒执行,循环延期 // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { try { //TODO... 业务逻辑代码 } finally { ztLock.unlock(); } return ""; } } }4、几点说明加锁的时候注意一下锁的粒度,粒度越小性能越好,比如商品的话,可以按照商品id进行锁。为了保证缓存一致性,可以使用读写锁。
2021年09月13日
206 阅读
0 评论
0 点赞
2021-09-03
Springboot整合Spring Retry实现重试机制
在项目开发过程中,经常会有这样的情况:第一次执行一个操作不成功,考虑到可能是网络原因造成,就多执行几次操作,直到得到想要的结果为止,这就是重试机制。Springboot可以通过整合Spring Retry框架实现重试。下面讲一下在之前新建的ibatis项目基础上整合Spring Retry框架的步骤:1、首先要在pom.xml配置中加入spring-retry的依赖:<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> 2、在启动类中加入重试注解@EnableRetry。import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.retry.annotation.EnableRetry; @EnableRetry //重试注解 @MapperScan("com.batis.mapper") @SpringBootApplication public class BatisApplication { public static void main(String[] args) { SpringApplication.run(BatisApplication.class, args); } } 3、新建重试接口RetryService和实现类RetryServiceImpl重试接口:public interface RetryService { void retryTransferAccounts(int fromAccountId, int toAccountId, float money) throws Exception; } 接口实现类:import com.batis.mapper.AccountMapper; import com.batis.model.Account; import com.batis.service.RetryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class RetryServiceImpl implements RetryService { @Autowired private AccountMapper accountMapper; @Transactional @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 3000, multiplier = 1, maxDelay = 10000)) @Override public void retryTransferAccounts(int fromAccountId, int toAccountId, float money) throws Exception { Account fromAccount = accountMapper.findOne(fromAccountId); fromAccount.setBalance(fromAccount.getBalance() - money); accountMapper.update(fromAccount); int a = 2 / 0; Account toAccount = accountMapper.findOne(toAccountId); toAccount.setBalance(toAccount.getBalance() + money); accountMapper.update(toAccount); throw new Exception(); } @Recover public void recover(Exception e) { System.out.println("回调方法执行!!!"); } } @Retryable:标记当前方法会使用重试机制value:重试的触发机制,当遇到Exception异常的时候,会触发重试maxAttempts:重试次数(包括第一次调用)delay:重试的间隔时间multiplier:delay时间的间隔倍数maxDelay:重试次数之间的最大时间间隔,默认为0,如果小于delay的设置,则默认为30000L@Recover:标记方法为回调方法,传参与@Retryable的value值需一致4、新建重试控制器类RetryControllerimport com.batis.service.RetryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/retry") public class RetryController { @Autowired private RetryService retryService; @RequestMapping(value = "/transfer", method = RequestMethod.GET) public String transferAccounts() { try { retryService.retryTransferAccounts(1, 2, 200); return "ok"; } catch (Exception e) { return "no"; } } } 5、启动ibatis项目进行测试,在浏览器地址栏输入:http://localhost:8080/retry/transfer可以看到,转账操作一共执行了3次,最后执行了回调方法。至此Springboot整合Spring Retry的步骤已经完成,测试也非常成功!
2021年09月03日
36 阅读
0 评论
0 点赞
2021-09-03
服务端如何防止订单重复支付?
如图是一个简化的下单流程,首先是提交订单,然后是支付。支付的话,一般是走支付网关(支付中心),然后支付中心与第三方支付渠道(微信、支付宝、银联)交互,支付成功以后,异步通知支付中心,支付中心更新自身支付订单状态,再通知业务应用,各业务再更新各自订单状态。这个过程中经常可能遇到的问题是掉单,无论是超时未收到回调通知也好,还是程序自身报错也好,总之由于各种各样的原因,没有如期收到通知并正确的处理后续逻辑等等,都会造成用户支付成功了,但是服务端这边订单状态没更新,这个时候有可能产生投诉,或者用户重复支付。由于③⑤造成的掉单称之为外部掉单,由④⑥造成的掉单我们称之为内部掉单为了防止掉单,这里可以这样处理:支付订单增加一个中间状态“支付中”,当同一个订单去支付的时候,先检查有没有状态为“支付中”的支付流水,当然支付(prepay)的时候要加个锁。支付完成以后更新支付流水状态的时候再讲其改成“支付成功”状态。支付中心这边要自己定义一个超时时间(比如:30秒),在此时间范围内如果没有收到支付成功回调,则应调用接口主动查询支付结果,比如10s、20s、30s查一次,如果在最大查询次数内没有查到结果,应做异常处理支付中心收到支付结果以后,将结果同步给业务系统,可以发MQ,也可以直接调用,直接调用的话要加重试(比如:SpringBoot Retry)无论是支付中心,还是业务应用,在接收支付结果通知时都要考虑接口幂等性,消息只处理一次,其余的忽略。业务应用也应做超时主动查询支付结果。对于上面说的超时主动查询可以在发起支付的时候将这些支付订单放到一张表中,用定时任务去扫为了防止订单重复提交,可以这样处理:1、创建订单的时候,用订单信息计算一个哈希值,判断redis中是否有key,有则不允许重复提交,没有则生成一个新key,放到redis中设置个过期时间,然后创建订单。其实就是在一段时间内不可重复相同的操作附上微信支付最佳实践:
2021年09月03日
34 阅读
0 评论
0 点赞
2021-08-16
通过输入Html提取其中的图片地址信息
/** * 通过Html获取图片链接 * @param inputHtml * @return */ public List<String> getDetailImagesListFromHtml(String inputHtml){ List<String> detailImagesList = ReUtil.findAll("url\(//(.*?(png|jpg|jpeg|webp|svg|psd|bmp|tif))", input, 1); return detailImagesList; }
2021年08月16日
55 阅读
0 评论
0 点赞
2021-04-07
分享一个我正在使用的CookieUtils工具类
CookieUtils包含功能得到Cookie的值,是否解编码设置Cookie的值 不设置生效时间默认浏览器关闭即失效设置Cookie的值 在指定时间内生效删除Cookie带cookie域名设置Cookie的值,并使其在指定时间内生效得到cookie的域名判断是否是一个IP代码如下:import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; public final class CookieUtils { final static Logger logger = LoggerFactory.getLogger(CookieUtils.class); /** * * @Description: 得到Cookie的值, 不编码 * @param request * @param cookieName * @return */ public static String getCookieValue(HttpServletRequest request, String cookieName) { return getCookieValue(request, cookieName, false); } /** * * @Description: 得到Cookie的值 * @param request * @param cookieName * @param isDecoder * @return */ public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) { Cookie[] cookieList = request.getCookies(); if (cookieList == null || cookieName == null) { return null; } String retValue = null; try { for (int i = 0; i < cookieList.length; i++) { if (cookieList[i].getName().equals(cookieName)) { if (isDecoder) { retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8"); } else { retValue = cookieList[i].getValue(); } break; } } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return retValue; } /** * * @Description: 得到Cookie的值 * @param request * @param cookieName * @param encodeString * @return */ public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) { Cookie[] cookieList = request.getCookies(); if (cookieList == null || cookieName == null) { return null; } String retValue = null; try { for (int i = 0; i < cookieList.length; i++) { if (cookieList[i].getName().equals(cookieName)) { retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString); break; } } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return retValue; } /** * * @Description: 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码 * @param request * @param response * @param cookieName * @param cookieValue */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue) { setCookie(request, response, cookieName, cookieValue, -1); } /** * * @Description: 设置Cookie的值 在指定时间内生效,但不编码 * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage) { setCookie(request, response, cookieName, cookieValue, cookieMaxage, false); } /** * * @Description: 设置Cookie的值 不设置生效时间,但编码 * 在服务器被创建,返回给客户端,并且保存客户端 * 如果设置了SETMAXAGE(int seconds),会把cookie保存在客户端的硬盘中 * 如果没有设置,会默认把cookie保存在浏览器的内存中 * 一旦设置setPath():只能通过设置的路径才能获取到当前的cookie信息 * @param request * @param response * @param cookieName * @param cookieValue * @param isEncode */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, boolean isEncode) { setCookie(request, response, cookieName, cookieValue, -1, isEncode); } /** * * @Description: 设置Cookie的值 在指定时间内生效, 编码参数 * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage * @param isEncode */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) { doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode); } /** * * @Description: 设置Cookie的值 在指定时间内生效, 编码参数(指定编码) * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage * @param encodeString */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) { doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString); } /** * * @Description: 删除Cookie带cookie域名 * @param request * @param response * @param cookieName */ public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) { doSetCookie(request, response, cookieName, null, -1, false); // doSetCookie(request, response, cookieName, "", -1, false); } /** * * @Description: 设置Cookie的值,并使其在指定时间内生效 * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage cookie生效的最大秒数 * @param isEncode */ private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) { try { if (cookieValue == null) { cookieValue = ""; } else if (isEncode) { cookieValue = URLEncoder.encode(cookieValue, "utf-8"); } Cookie cookie = new Cookie(cookieName, cookieValue); if (cookieMaxage > 0) cookie.setMaxAge(cookieMaxage); if (null != request) {// 设置域名的cookie String domainName = getDomainName(request); logger.info("========== domainName: {} ==========", domainName); if (!"localhost".equals(domainName)) { cookie.setDomain(domainName); } } cookie.setPath("/"); response.addCookie(cookie); } catch (Exception e) { e.printStackTrace(); } } /** * * @Description: 设置Cookie的值,并使其在指定时间内生效 * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage cookie生效的最大秒数 * @param encodeString */ private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) { try { if (cookieValue == null) { cookieValue = ""; } else { cookieValue = URLEncoder.encode(cookieValue, encodeString); } Cookie cookie = new Cookie(cookieName, cookieValue); if (cookieMaxage > 0) cookie.setMaxAge(cookieMaxage); if (null != request) {// 设置域名的cookie String domainName = getDomainName(request); logger.info("========== domainName: {} ==========", domainName); if (!"localhost".equals(domainName)) { cookie.setDomain(domainName); } } cookie.setPath("/"); response.addCookie(cookie); } catch (Exception e) { e.printStackTrace(); } } /** * * @Description: 得到cookie的域名 * @return */ private static final String getDomainName(HttpServletRequest request) { String domainName = null; String serverName = request.getRequestURL().toString(); if (serverName == null || serverName.equals("")) { domainName = ""; } else { serverName = serverName.toLowerCase(); serverName = serverName.substring(7); final int end = serverName.indexOf("/"); serverName = serverName.substring(0, end); if (serverName.indexOf(":") > 0) { String[] ary = serverName.split("\\:"); serverName = ary[0]; } final String[] domains = serverName.split("\\."); int len = domains.length; if (len > 3 && !isIp(serverName)) { // www.xxx.com.cn domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1]; } else if (len <= 3 && len > 1) { // xxx.com or xxx.cn domainName = "." + domains[len - 2] + "." + domains[len - 1]; } else { domainName = serverName; } } return domainName; } public static String trimSpaces(String IP){//去掉IP字符串前后所有的空格 while(IP.startsWith(" ")){ IP= IP.substring(1,IP.length()).trim(); } while(IP.endsWith(" ")){ IP= IP.substring(0,IP.length()-1).trim(); } return IP; } public static boolean isIp(String IP){//判断是否是一个IP boolean b = false; IP = trimSpaces(IP); if(IP.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")){ String s[] = IP.split("\\."); if(Integer.parseInt(s[0])<255) if(Integer.parseInt(s[1])<255) if(Integer.parseInt(s[2])<255) if(Integer.parseInt(s[3])<255) b = true; } return b; } }
2021年04月07日
45 阅读
0 评论
0 点赞
2021-03-17
支付宝支付前后端实现(Vue+Spring Boot)
本文主要总结基于Vue/Spring Boot的支付宝支付实现,兼容H5与电脑端。1、应用创建与配置第一步:登录支付宝开放平台创建应用; 并视情况需要添加“手机网站支付”和“电脑网站支付”能力;手机网站支付可通过浏览器打开支付宝应用程序进行付款,但在微信公众号打开的页面无法通过支付宝进行支付,电脑端的可通过二维码或者登录支付宝账号付款。第二步:密钥配置包括应用私钥、应用公钥和支付宝公钥。可通过支付宝开放平台开发助手生成应用私钥与应用公钥,然后在开放平台中将应用公钥配置到应用中,生成支付宝公钥。最终程序中需要使用到应用私钥与支付宝公钥,其中应用私钥用于对发往支付宝的消息进行加密,而支付宝公钥用于对支付宝返回的数据进行验签。具体配置过程如下:使用支付宝开放平台助手生成应用私钥与公钥切记生成后将私钥与公钥另外保存;登录开放平台配置应用公钥点击接口加签方式中的设置按钮,在弹出窗口中将应用公钥复制进去,然后生成支付宝公钥,将公钥复制下来保存,后续在代码中会需要使用到。生成支付宝公钥后应用公钥基本就用不着了,后续的代码主要使用的是应用私钥和支付宝公钥。2、处理流程概述支付处理步骤如下所示:3、业务的具体实现3.1 前端发起具体支付界面视业务场景而定,用户选择或填写相关信息后,点击确定按钮时调用后端接口生成订单编号,然后将相关信息展示给用户确认,用户确认后再发起付款流程。发起付款流程前端关键代码如下所示:this.$post("/pay/refill", { orderNo: this.orderNo, fee: this.selectedPlan.refillMoney, channel: this.refillChannel === "wx" ? 2 : 1, tbAmount: this.selectedPlan.payMoney, tbSentAmount: this.isVip ? this.selectedPlan.payMoney * 0.5 : 0, }).then((resp) => { this.refillCompleted = true; if (this.refillChannel === "wx") { this.$goPath("/user/pay/wx-pay", resp); } else { // 支付宝 // 打开单独的支付宝页面 this.$goPath("/user/pay/ali-pay", resp); } });注意我的项目支持微信与支付宝两种支付方式,因此前端选择完后会将所选择的方式、金额等信息传给后台生成支付表单3.2 支付表单生成1.依赖包下载:后端需要使用到阿里的包,maven地址:<dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.10.111.ALL</version> </dependency>2.订单生成前端发起订单生成请求后,后端生成订单记录,并根据微信还是支付宝来判断调用微信还是支付宝接口完成支付表单生成。/** * 充值 * * @param refill 充值 * @return 支付相关信息 */ @PreAuthorize("isAuthenticated()") @PostMapping("/refill") public PayDTO refill(@RequestBody @Validated RefillDTO refill) { UserDTO user = this.getLoginUserOrThrow(); if (null == tradeService.findByNo(refill.getOrderNo())) { // 保存订单 TradeDTO tradeDTO = new TradeDTO(); ... tradeService.save(user, tradeDTO); } // 获取支付表单信息 if (refill.getChannel().equals(TradeChannel.WX)) { // 微信 String openId = user.getWxOpenId(); if (refill.getPlatform().equals(1)) { openId = user.getMpOpenId(); } return wxPayService.getPayUrl(openId, refill.getOrderNo(), refill.getFee(), TradeType.REFILL, refill.getPlatform()); } else if (refill.getChannel().equals(TradeChannel.ALIPAY)) { // 支付宝 return aliPayService.getPayForm(refill.getOrderNo(), refill.getFee(), TradeType.REFILL, refill.getPlatform()); } else { throw new UnsupportedOperationException("不支持的支付渠道"); } }3.支付表单生成/** * 获取支付表单 * * @param orderNo 订单号 * @param fee 订单金额 * @param tradeType 交易类型 * @param platform 平台,0:WEB;1:移动端 * @return 下单表单 */ public PayDTO getPayForm(String orderNo, Double fee, TradeType tradeType, Integer platform) { boolean isH5 = Optional.ofNullable(platform).orElse(0).equals(1); AlipayClient alipayClient = new DefaultAlipayClient(API_URL, APP_ID, APP_KEY, "json", "utf-8", ZFB_PUBLIC_KEY, SIGN_TYPE); Map<String, String> params = MapEnhancer.<String, String>create() .put("out_trade_no", orderNo) .put("total_amount", String.format("%.2f", fee)) .put("subject", tradeType.getName()) .put("product_code", "FAST_INSTANT_TRADE_PAY") .get(); String body; try { if (isH5) { if (logger.isDebugEnabled()) { logger.debug("return url: {}", h5ReturnUrl); } AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); request.setBizContent(JSONObject.toJSONString(params)); request.setReturnUrl(h5ReturnUrl); request.setNotifyUrl(notifyUrl); body = alipayClient.pageExecute(request).getBody(); } else { AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); request.setBizContent(JSONObject.toJSONString(params)); request.setReturnUrl(returnUrl); request.setNotifyUrl(notifyUrl); body = alipayClient.pageExecute(request).getBody(); } } catch (AlipayApiException e) { logger.error("生成支付宝支付表单失败", e); throw BusinessException.create("生成支付宝支付表单失败"); } if (logger.isDebugEnabled()) { logger.debug("支付表单内容: {}", body); } PayDTO payDTO = new PayDTO(); payDTO.setFee(fee); payDTO.setOrderNo(orderNo); payDTO.setCodeUrl(body); return payDTO; }其中API_URL为支付宝网关地址(https://openapi.alipay.com/gateway.do),APP_ID为支付宝开放平台中的应用id, APP_KEY为前面密钥配置中的应用私钥;ZFB_PUBLIC_KEY为密钥配置中的支付宝公钥。注意需要根据h5及pc网站类型来判断使用哪个API;API关键参数如下:调用接口成功后将请求返回的数据与订单号、交易金额一起返回给前端,由前端进行下一步处理。另外在调用接口时,还需要指定returnUrl与notifyUrl,其中returnUrl是支付宝在用户支付完成后跳转到的页面;notifyUrl是用于接收支付宝支付结果的地址(一般为后端地址,由支付宝调用)。3.3 前端跳转处理前端接收到后端生成的支付表单后,跳转到一个专门的页面,将返回的内容加载到页面中,并自动触发表单提交,处理如下:(ali-pay.vue)<template> <!-- 支付宝支付界面 --> <div class="ali-pay box-shadow mt-4 border-radius"> <div v-html="payInfo.codeUrl" ref="pay"></div> </div> </template> <script> export default { components: {}, props: [], data() { return { payInfo: {}, }; }, mounted() { this.payInfo = this.$route.query; this.$nextTick(() => { this.$refs.pay.children[0].submit(); }); }, methods: {}, }; </script> <style lang="scss"> .ali-pay { } </style>经过以上处理,h5端将会调起支付宝软件进行支付,web端则会显示二维码或账号输入页面。3.4 支付返回页面处理及支付状态更新用户支付完成后,支付宝将会返回生成表单时指定的returnUrl地址,同时会附带订单号在查询参数中;在这个页面中我们可以根据订单号,通过后台调用支付宝接口查询订单状态,然后依此来更新系统订单支付状态。前端页面如下(待完善,可以在trade/status接口返回查询的订单状态,在页面做相应提示):<template> <!-- 支付宝支付界面 --> <div class="ali-pay-success box-shadow mt-4 border-radius p-4"> <div> 支付成功,您可以进入 <el-button type="text" @click="$goPath('/user/account/refill-history')" >充值记录</el-button >查看历史记录 </div> </div> </template> <script> export default { components: {}, props: [], data() { return { payInfo: {}, }; }, mounted() { this.payInfo = this.$route.query; // 更新支付状态 this.$get("/trade/status", { no: this.payInfo.out_trade_no }); }, methods: {}, }; </script><style lang="scss"> .ali-pay-success { } </style>后端/trade/status接口实现如下:/** * 查询交易状态 * * @param no 交易编号 * @return 交易状态,0:失败,1:成功,2:未知 */ @GetMapping("/status") public Integer getTradeStatus(@RequestParam("no") String no) { TradeDTO trade = tradeService.findByNo(no); if (null == trade) { throw BusinessException.create("订单不存在"); } if (trade.getState() == 1 || trade.getState() == 0) { return trade.getState(); } // 交易状态未知时,通过相关支付服务查询状态 if (trade.getChannel().equals(TradeChannel.WX)) { return wxPayService.getTradeStatus(trade); } else if (trade.getChannel().equals(TradeChannel.ALIPAY)) { return aliPayService.getTradeStatus(trade); } return trade.getState(); }这里先在系统里面查询订单状态(状态可能已经变更,因为我们同时会接收支付宝支付成功的通知),如果订单状态未知,那么通过支付宝或微信支付的接口更新订单状态。支付宝支付状态查询接口实现:/** * 获取订单状态 * * @param trade 订单信息 * @return 订单状态 */ public Integer getTradeStatus(TradeDTO trade) { AlipayClient alipayClient = new DefaultAlipayClient(API_URL, APP_ID, APP_KEY, "json", "utf-8", ZFB_PUBLIC_KEY, SIGN_TYPE); AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); Map<String, String> params = MapEnhancer.<String, String>create() .put("out_trade_no", trade.getNo()) .get(); request.setBizContent(JSONObject.toJSONString(params)); AlipayTradeQueryResponse response; try { response = alipayClient.execute(request); } catch (AlipayApiException e) { logger.error("查询订单状态失败", e); return trade.getState(); } if (logger.isDebugEnabled()) { logger.debug("订单信息: {}", JSONObject.toJSONString(response)); } if (response.isSuccess()) { String tradeStatus = response.getTradeStatus(); if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) { tradeService.tradeSuccess(trade); } else if (tradeStatus.equals("TRADE_CLOSED")) { tradeService.tradeFailed(trade); } } return trade.getState(); }注意查询到结果后,需要更新交易状态,并视情况进行下一步的业务处理,如用户充值的就得增加用户余额等。3.5 支付结果接收至此支付过程基本已经完成。但某些情况用户支付完成后并未返回我们指定的页面,如用户可能直接关闭浏览器等极端情况,因此必须与支付宝推送结果的方式结合,来防止状态不一致。接收支付宝支付结果的接口实现如下:@RequestMapping("ali-notify") public String aliNotify(@RequestParam Map<String, String> params) { if (logger.isDebugEnabled()) { logger.debug("收到支付宝通知: {}", params); } return aliPayService.parseAndSaveTradeResult(params); }具体service方法实现如下:/** * 解析通知结果并保存 * * @param params 通知结果 */ public String parseAndSaveTradeResult(Map<String, String> params) { if (logger.isDebugEnabled()) { logger.debug("接收到支付宝支付结果: {}", params); } // 结果验签 boolean signVerified; try { signVerified = AlipaySignature.rsaCheckV1(params, ZFB_PUBLIC_KEY, "utf-8", SIGN_TYPE); } catch (AlipayApiException e) { logger.error("验签失败", e); return "failure"; } if (!signVerified) { logger.warn("验签失败,通知内容:{}", params); return "failure"; } String orderNo = MapUtils.getString(params, "out_trade_no", ""); if (StringUtils.isBlank(orderNo)) { logger.warn("订单号为空,通知内容:{}", params); return "success"; } // 根据订单号查询订单 TradeDTO trade = tradeService.findByNo(orderNo); if (null == trade) { logger.warn("订单不存在,通知内容:{}", params); return "success"; } if (trade.getState() == 1) { logger.debug("订单已成功"); return "success"; } String tradeStatus = MapUtils.getString(params, "trade_status", ""); if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) { trade.setState(1); tradeService.tradeSuccess(trade); } else { trade.setState(0); tradeService.tradeFailed(trade); } return "success"; }注意如果查询到支付状态是成功的,就需要进行下一步的业务处理了,如增加用户余额等。实际交易成功处理,因为同一笔订单可能会有两个进程更新交易状态(通道消息和主动查询的消息),这两个操作肯定只能有一个操作能够生效;如果是单节点情况,可以通过JVM锁来控制;如果多节点的,就需要通过分页式锁来控制了。我的项目里面是采用了Redis实现了个简单的分布式锁处理,具体实现方案网上一大堆,不在此展开。另外,可以看到我的代码里面有很多关于微信支付与支付宝支付的判断,这是因为我同时支持了微信支付与支付宝支付;这两者的支付过程基本类似,本文主要讲的是支付宝支付,微信的未展开,后续再单独对微信支付实现做一个补充。
2021年03月17日
122 阅读
0 评论
0 点赞
2021-03-14
JAVA秒杀系统的简单实现(Redis+RabbitMQ)
1、分析秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。秒杀业务流程比较简单,一般就是下订单减库存。上述三点的主要问题就是在高并发的情况下保证数据的一致性。2、使用的技术和架构2.1 秒杀架构图2.2 实现流程使用 redis 缓存秒杀的商品信息,秒杀成功后使用消息队列发送订单信息,然后将更新后数据重新写入redis。RabbitMQ监听器在接受到消息后,将订单信息写入数据库。在秒杀时使用redisson对商品信息上锁。2.3 流程图3、准备工作3.1 安装redis cluster教程一大堆,这里我就不多赘述了,可以参考:https://blog.csdn.net/CFrieman/article/details/835830853.2 安装RabbitMQ和erlang教程一大堆,这里我就不多赘述了,可以参考:https://blog.csdn.net/qq_36505948/article/details/827341334、具体实现4.1 SeckillServicepublic class SeckillService { @Autowired private RedisClusterClient rt; @Autowired private SeckillMapper sm; @Autowired private RedissonClient redissonClient; // 加锁 @Autowired private RabbitmqSendMessage rsm; @Autowired private SecorderMapper om; /** * 初始化 ,将mysql中的商品信息缓存到redis中 * @return */ public List<Seckill> querySeckill() { List<Seckill> list = (List<Seckill>) rt.get("secgoods"); if(list==null) { list = sm.selectByExample(null); rt.set("secgoods", list, 60*30); } return list; } public boolean queryStartTime(Seckill sec) { Date date = new Date();// 比较时间,是否到秒杀时间 Date startTime = sec.getStarttime(); // 秒杀活动还未开始 if (startTime.getTime() > date.getTime()) { return false; } return true; } // 减库存redis public void decreaseStock(String id) { int goodsid = Integer.parseInt(id); List<Seckill> list = (List<Seckill>) rt.get("secgoods"); if (list!=null) { for (Seckill sec : list) { if (goodsid==sec.getId()) { sec.setCount(sec.getCount()-1); //写回redis rt.set("secgoods", list, 60*30); return ; } } } } // public Seckill findSec(String secid) { List<Seckill> list = (List<Seckill>) rt.get("secgoods"); int id = Integer.parseInt(secid); for(Seckill sec:list) { if(sec.getId()==id) { return sec; } } return null; } // 开始秒杀 public String goSeckill(String goodsid, String username) { String key = username + ":" + goodsid; String secid = goodsid; Long value = (Long) rt.get(key); if (value != null) { return "exist"; } Seckill sec = findSec(secid); boolean flag = queryStartTime(sec); if (!flag) { return "notTime"; } RLock rLock = redissonClient.getLock("miaosha"); rLock.lock(); if (sec.getCount() > 0) { decreaseStock(goodsid); // 减少库存 rt.set(key, System.currentTimeMillis(), 60*30); Secorder newOrder = new Secorder(); newOrder.setCreatetime(new Date()); newOrder.setGoodsid(Integer.parseInt(goodsid)); newOrder.setStatus("未付款"); newOrder.setUsername(username); String json = JSONObject.toJSONString(newOrder); rsm.send(json); // 异步下单 rLock.unlock(); // 解锁 return "success"; } else { rLock.unlock(); return "failed"; } } // 写入mysql public void saveOrder(String json) { Secorder order = JSON.parseObject(json, Secorder.class); int n = sm.updateCount(order.getGoodsid()); int m = om.insert(order); } }4.2 RabbitmqListenner@Service public class RabbitmqListenner implements MessageListener { @Autowired private SeckillService ss; @Override public void onMessage(Message msg) { byte[] data = msg.getBody(); try { String json = new String(data,"utf-8"); System.out.println(json); ss.saveOrder(json); //将监听到的订单写入MySQL } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }4.3 RabbitmqSendMessagepublic class RabbitmqSendMessage { @Autowired private RabbitTemplate rt; private final String QUEEN_NAME = "MIAOSHA"; /** * 发送消息 * @param msg */ public void send(String msg) { rt.convertAndSend(QUEEN_NAME,msg); } }4.4以上就是整个业务流程的核心代码,使用redisson保证数据一致性,用rabbitmq异步下单将下单及写数据库这个长操作变成两个短操作。GitHub源码地址,关于数据库建表什么的,大家直接去源码里看吧。5.优化限流:使用验证码,请求秒杀接口需要验证图形验证码的正确性,这样也很好的防止脚本的不断访问;防刷:一个用户对一个路径的访问次数在一定时间内有限制,使用redis可以解决接口地址隐藏:接口地址传参,保证秒杀接口不是一个固定路径,防止接口被刷,同时也可以有效隐藏秒杀地址。
2021年03月14日
209 阅读
0 评论
1 点赞
2021-03-12
SpringBoot+RabbitMQ实现手动Consumer Ack
一、Consumer Ack的三种方式自动确认:acknowledge = “none”,这是默认的方式,如果不配置的话,默认就是自动确认,消费方从消息队列中拿出消息后,消息队列中都会清除掉这条消息(不安全).手动确认:acknowledge = “manual”,手动确认就是当消费者取出来消息其后的操作正常执行后,返回给消息队列,让其清除该条消息;如果后续执行有异常,可以设置requeue=true返回其消息队列,再让其消息队列重新给消费者发送消息.根据异常情况确认(很麻烦):acknowledge = “auto”.二、SpringBoot+RabbitMQ实现手动Consumer Ack1.pom文件中导入依赖坐标<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>2.在生产者与消费者工程yml配置文件中开启手动Ackspring: rabbitmq: host: 192.168.253.128 #ip username: guest password: guest virtual-host: / port: 5672 listener: simple: acknowledge-mode: manual #开启手动Ack3.在生产者工程中创建一个配置类声明队列与交换机的关系import org.springframework.amqp.core.*; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitMQConfig { //交换机的名称; public static final String DIRECT_EXCHANGE_NAME = "direct_boot_exchange"; //队列名称; public static final String DIRECT_QUEUE_NAME = "direct_boot_queue"; /** * 声明交换机,在以后我们会定义多个交换机, * 所以给这个注入的Bean起一个名字,同理在绑定的时候用@Qualifier注解; * durablie:持久化 */ @Bean("directExchange") public Exchange directExchange(){ return ExchangeBuilder