Docker常用命令 , Springboot切面实现权限校验 , Springboot五种热部署方式, 基于springboot实现MySQL读写分离技术 , springboot注解大全. SpringBoot第三方jar包打包问题 若依框架二级域名配置 Docker常用命令 1、Docker容器信息 1 2 3 4 5 6 # docker version # docker info # docker --help
2、镜像操作 提示:对于镜像的操作可使用镜像名、镜像长ID和短ID。
2.1、镜像查看 1 2 3 4 # docker images # docker images -a
1 2 3 4 # docker images -q # docker images -qa
1 2 3 4 # docker images --digests # docker images --no-trunc
1 2 3 4 5 6 7 8 # docker search mysql # docker search --filter=stars=600 mysql # docker search --no-trunc mysql # docker search --automated mysql
2.2、镜像搜索 1 2 3 4 5 6 7 8 # docker search mysql # docker search --filter=stars=600 mysql # docker search --no-trunc mysql # docker search --automated mysql
2.3、镜像下载 1 2 3 4 5 6 # docker pull redis # docker pull -a redis # docker pull bitnami/redis
2.4、镜像删除 1 2 3 4 5 6 7 8 # docker rmi redis # docker rmi -f redis # docker rmi -f redis tomcat nginx # docker rmi -f $(docker images -q)
2.5、镜像构建 1 2 3 4 5 # cd /docker/dockerfile vim mycentos # docker build -f /docker/dockerfile/mycentos -t mycentos:1.1
3、容器操作 提示:对于容器的操作可使用CONTAINER ID 或 NAMES。
3.1、容器启动 1 2 3 4 # docker run -i -t --name mycentos # docker run -d mycentos
注意:此时使用”docker ps -a”会发现容器已经退出。这是docker的机制:要使Docker容器后台运行,就必须有一个前台进程。解决方案:将你要运行的程序以前台进程的形式运行。
1 2 3 4 # docker start redis # docker restart redis
3.2、容器进程 1 2 3 4 5 # # docker top redis # for i in `docker ps |grep Up|awk '{print $1}'`;do echo \ &&docker top $i; done
3.3、容器日志 1 2 3 4 5 6 # docker logs rabbitmq # docker logs -f -t --tail=20 redis # docker logs --since="2019-05-21" --tail=10 redis
3.4、容器的进入与退出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # docker run -it centos /bin/bash # exit # 快捷键:Ctrl + P + Q # docker attach --sig-proxy=false centos # docker exec -i -t centos /bin/bash # docker exec -i -t centos ls -l /tmp # docker exec -d centos touch cache.txt
3.5、查看容器 1 2 3 4 5 6 7 8 # docker ps # docker ps -q # docker ps -a # docker ps -s
1 2 3 4 5 6 # docker ps -l # docker ps -n 3 # docker ps --no-trunc
1 2 3 4 # docker inspect redis # docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' redis
3.6、容器的停止与删除 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # docker stop redis # docker kill redis # docker rm redis # docker rm -f redis # docker rm -f $(docker ps -a -q) docker ps -a -q | xargs docker rm # docker rm -l db # docker rm -v redis
3.7、生成镜像 1 2 # docker commit -a="DeepInThought" -m="my redis" [redis容器ID] myredis:v1.1
3.8、容器与主机间的数据拷贝 1 2 3 4 5 6 7 # docker cp rabbitmq:/[container_path] [local_path] # docker cp [local_path] rabbitmq:/[container_path]/ # docker cp [local_path] rabbitmq:/[container_path] 推荐阅读
理解AOP
AOP实例
AOP相关注解
@Pointcut @Around @Before @After @AfterReturning @AfterThrowing SpringBoot:切面AOP实现权限校验:实例演示与注解全解 1 理解AOP 1.1 什么是AOP AOP(Aspect Oriented Programming),面向切面思想,是Spring的三大核心思想之一(两外两个:IOC-控制反转、DI-依赖注入)。
那么AOP为何那么重要呢?在我们的程序中,经常存在一些系统性的需求,比如权限校验、日志记录、统计等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护。例如下面这个示意图: 有多少业务操作,就要写多少重复的校验和日志记录代码,这显然是无法接受的。当然,用面向对象的思想,我们可以把这些重复的代码抽离出来,写成公共方法,就是下面这样:
这样,代码冗余和可维护性的问题得到了解决,但每个业务方法中依然要依次手动调用这些公共方法,也是略显繁琐。有没有更好的方式呢?有的,那就是AOP,AOP将权限校验、日志记录等非业务代码完全提取出来,与业务代码分离,并寻找节点切入业务代码中:
1.2 AOP体系与概念 简单地去理解,其实AOP要做三类事:
在哪里切入,也就是权限校验等非业务操作在哪些业务代码中执行。 在什么时候切入,是业务代码执行前还是执行后。 切入后做什么事,比如做权限校验、日志记录等。 因此,AOP的体系可以梳理为下图: 一些概念详解:
Pointcut
:切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为execution
方式和annotation
方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。Advice
:处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。Aspect
:切面,即Pointcut
和Advice
。Joint point
:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。Weaving
:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。网络上有张图,我觉得非常传神,贴在这里供大家观详:
2 AOP实例 实践出真知,接下来我们就撸代码来实现一下AOP。完成项目已上传至:
https://github.com/ThinkMugz/aopDemo
使用 AOP,首先需要引入 AOP 的依赖。参数校验:这么写参数校验(validator)就不会被劝退了~
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
2.1 第一个实例 接下来,我们先看一个极简的例子:所有的get
请求被调用前在控制台输出一句”get请求的advice触发了”。
具体实现如下:
创建一个AOP切面类,只要在类上加个 @Aspect
注解即可。@Aspect
注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component
注解将该类交给 Spring 来管理。在这个类里实现advice: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.mu.demo.advice;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Aspect @Component public class LogAdvice { @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)") private void logAdvicePointcut () {} @Before("logAdvicePointcut()") public void logAdvice () { System.out.println("get请求的advice触发了" ); } }
创建一个接口类,内部创建一个get请求: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.mu.demo.controller;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import org.springframework.web.bind.annotation.*;@RestController @RequestMapping(value = "/aop") public class AopController { @GetMapping(value = "/getTest") public JSONObject aopTest () { return JSON.parseObject("{\"message\":\"SUCCESS\",\"code\":200}" ); } @PostMapping(value = "/postTest") public JSONObject aopTest2 (@RequestParam("id") String id) { return JSON.parseObject("{\"message\":\"SUCCESS\",\"code\":200}" ); } }
项目启动后,请求http://localhost:8085/aop/getTest
接口:
在这里插入图片描述
请求http://localhost:8085/aop/postTest
接口,控制台无输出,证明切点确实是只针对被GetMapping
修饰的方法。
2.2 第二个实例 下面我们将问题复杂化一些,该例的场景是:
自定义一个注解PermissionsAnnotation
创建一个切面类,切点设置为拦截所有标注PermissionsAnnotation
的方法,截取到接口的参数,进行简单的权限校验 将PermissionsAnnotation
标注在测试接口类的测试接口test
上 具体的实现步骤:
使用@Target、@Retention、@Documented
自定义一个注解: 1 2 3 4 5 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface PermissionAnnotation{}
创建第一个AOP切面类,,只要在类上加个 @Aspect
注解即可。@Aspect
注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component
注解将该类交给 Spring 来管理。在这个类里实现第一步权限校验逻辑: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.example.demo;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;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.springframework.core.annotation.Order;import org.springframework.stereotype.Component;@Aspect @Component @Order(1) public class PermissionFirstAdvice { @Pointcut("@annotation(com.mu.demo.annotation.PermissionAnnotation)") private void permissionCheck () { } @Around("permissionCheck()") public Object permissionCheckFirst (ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("===================第一个切面===================:" + System.currentTimeMillis()); Object[] objects = joinPoint.getArgs(); Long id = ((JSONObject) objects[0 ]).getLong("id" ); String name = ((JSONObject) objects[0 ]).getString("name" ); System.out.println("id1->>>>>>>>>>>>>>>>>>>>>>" + id); System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name); if (id < 0 ) { return JSON.parseObject("{\"message\":\"illegal id\",\"code\":403}" ); } return joinPoint.proceed(); } }
创建接口类,并在目标方法上标注自定义注解 PermissionsAnnotation
: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.example.demo;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import org.springframework.web.bind.annotation.*;@RestController @RequestMapping(value = "/permission") public class TestController { @RequestMapping(value = "/check", method = RequestMethod.POST) @PermissionsAnnotation() public JSONObject getGroupList (@RequestBody JSONObject request) { return JSON.parseObject("{\"message\":\"SUCCESS\",\"code\":200}" ); } }
在这里,我们先进行一个测试。首先,填好请求地址和header:
其次,构造正常的参数:
可以拿到正常的响应结果:
然后,构造一个异常参数,再次请求:
响应结果显示,切面类进行了判断,并返回相应结果:
!
有人会问,如果我一个接口想设置多个切面类进行校验怎么办?这些切面的执行顺序如何管理?
很简单,一个自定义的AOP
注解可以对应多个切面类,这些切面类执行顺序由@Order
注解管理,该注解后的数字越小,所在切面类越先执行。
下面在实例中进行演示:
创建第二个AOP切面类,在这个类里实现第二步权限校验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.example.demo;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;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.springframework.core.annotation.Order;import org.springframework.stereotype.Component;@Aspect @Component @Order(0) public class PermissionSecondAdvice { @Pointcut("@annotation(com.example.demo.PermissionsAnnotation)") private void permissionCheck () { } @Around("permissionCheck()") public Object permissionCheckSecond (ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("===================第二个切面===================:" + System.currentTimeMillis()); Object[] objects = joinPoint.getArgs(); Long id = ((JSONObject) objects[0 ]).getLong("id" ); String name = ((JSONObject) objects[0 ]).getString("name" ); System.out.println("id->>>>>>>>>>>>>>>>>>>>>>" + id); System.out.println("name->>>>>>>>>>>>>>>>>>>>>>" + name); if (!name.equals("admin" )) { return JSON.parseObject("{\"message\":\"not admin\",\"code\":403}" ); } return joinPoint.proceed(); } }
重启项目,继续测试,构造两个参数都异常的情况:
响应结果,表面第二个切面类执行顺序更靠前:
3 AOP相关注解 上面的案例中,用到了诸多注解,下面针对这些注解进行详解。
3.1 @Pointcut @Pointcut
注解,用来定义一个切面,即上文中所关注的某件事情的入口,切入点定义了事件触发时机。
1 2 3 4 5 6 7 8 9 10 @Aspect @Component public class LogAspectHandler { @Pointcut("execution(* com.mutest.controller..*.*(..))") public void pointCut () {} }
@Pointcut 注解指定一个切面,定义需要拦截的东西,这里介绍两个常用的表达式:一个是使用 execution()
,另一个是使用 annotation()
。
更多参考:SpringBoot内容聚合
execution表达式:
以 execution(* * com.mutest.controller..*.*(..)))
表达式为例:
第一个 * 号的位置:表示返回值类型,* 表示所有类型。 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,在本例中指 com.mutest.controller包、子包下所有类的方法。 第二个 * 号的位置:表示类名,* 表示所有类。 (..):这个星号表示方法名, 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。annotation() 表达式:
annotation()
方式是针对某个注解来定义切面,比如我们对具有 @PostMapping 注解的方法做切面,可以如下定义切面:
1 2 @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") public void annotationPointcut () {}
然后使用该切面的话,就会切入注解是 @PostMapping
的所有方法。这种方式很适合处理 @GetMapping、@PostMapping、@DeleteMapping
不同注解有各种特定处理逻辑的场景。
还有就是如上面案例所示,针对自定义注解来定义切面。
1 2 @Pointcut("@annotation(com.example.demo.PermissionsAnnotation)") private void permissionCheck () {}
3.2 @Around @Around
注解用于修饰Around
增强处理,Around
增强处理非常强大,表现在:
@Around
可以自由选择增强动作与目标方法的执行顺序,也就是说可以在增强动作前后,甚至过程中执行目标方法。这个特性的实现在于,调用ProceedingJoinPoint
参数的procedd()
方法才会执行目标方法。@Around
可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。Around
增强处理有以下特点:
当定义一个Around
增强处理方法时,该方法的第一个形参必须是 ProceedingJoinPoint
类型(至少一个形参)。在增强处理方法体内,调用ProceedingJoinPoint
的proceed
方法才会执行目标方法:这就是@Around
增强处理可以完全控制目标方法执行时机、如何执行的关键;如果程序没有调用ProceedingJoinPoint
的proceed
方法,则目标方法不会执行。 调用ProceedingJoinPoint
的proceed
方法时,还可以传入一个Object[ ]
对象,该数组中的值将被传入目标方法作为实参——这就是Around
增强处理方法可以改变目标方法参数值的关键。这就是如果传入的Object[ ]
数组长度与目标方法所需要的参数个数不相等,或者Object[ ]
数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。 @Around
功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before
、AfterReturning
就能解决的问题,就没有必要使用Around
了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用Around
。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用Around
增强处理了。
下面,在前面例子上做一些改造,来观察@Around
的特点。
自定义注解类不变。首先,定义接口类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.example.demo;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import org.springframework.web.bind.annotation.*;@RestController @RequestMapping(value = "/permission") public class TestController { @RequestMapping(value = "/check", method = RequestMethod.POST) @PermissionsAnnotation() public JSONObject getGroupList (@RequestBody JSONObject request) { return JSON.parseObject("{\"message\":\"SUCCESS\",\"code\":200,\"data\":" + request + "}" ); } }
唯一切面类(前面案例有两个切面类,这里只需保留一个即可):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.example.demo;import com.alibaba.fastjson.JSONObject;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.springframework.core.annotation.Order;import org.springframework.stereotype.Component;@Aspect @Component @Order(1) public class PermissionAdvice { @Pointcut("@annotation(com.example.demo.PermissionsAnnotation)") private void permissionCheck () { } @Around("permissionCheck()") public Object permissionCheck (ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("===================开始增强处理===================" ); Object[] objects = joinPoint.getArgs(); Long id = ((JSONObject) objects[0 ]).getLong("id" ); String name = ((JSONObject) objects[0 ]).getString("name" ); System.out.println("id1->>>>>>>>>>>>>>>>>>>>>>" + id); System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name); JSONObject object = new JSONObject (); object.put("id" , 8 ); object.put("name" , "lisi" ); objects[0 ] = object; return joinPoint.proceed(objects); } }
同样使用JMeter调用接口,传入参数:{"id":-5,"name":"admin"}
,响应结果表明:@Around
截取到了接口的入参,并使接口返回了切面类中的结果。
3.3 @Before @Before
注解指定的方法在切面切入目标方法之前执行,可以做一些 Log
处理,也可以做一些信息的统计,比如获取用户的请求 URL
以及用户的 IP
地址等等,这个在做个人站点的时候都能用得到,都是常用的方法。例如下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Aspect @Component @Slf4j public class LogAspectHandler { @Before("pointCut()") public void doBefore (JoinPoint joinPoint) { log.info("====doBefore方法进入了====" ); Signature signature = joinPoint.getSignature(); String declaringTypeName = signature.getDeclaringTypeName(); String funcName = signature.getName(); log.info("即将执行方法为: {},属于{}包" , funcName, declaringTypeName); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); String url = request.getRequestURL().toString(); String ip = request.getRemoteAddr(); log.info("用户请求的url为:{},ip地址为:{}" , url, ip); } }
JointPoint
对象很有用,可以用它来获取一个签名,利用签名可以获取请求的包名、方法名,包括参数(通过 joinPoint.getArgs()
获取)等。
3.4 @After @After
注解和 @Before
注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Aspect @Component @Slf4j public class LogAspectHandler { @Pointcut("execution(* com.mutest.controller..*.*(..))") public void pointCut () {} @After("pointCut()") public void doAfter (JoinPoint joinPoint) { log.info("==== doAfter 方法进入了====" ); Signature signature = joinPoint.getSignature(); String method = signature.getName(); log.info("方法{}已经执行完" , method); } }
到这里,我们来写个 Controller 测试一下执行结果,新建一个 AopController 如下:
1 2 3 4 5 6 7 8 9 @RestController @RequestMapping("/aop") public class AopController { @GetMapping("/{name}") public String testAop (@PathVariable String name) { return "Hello " + name; } }
启动项目,在浏览器中输入:localhost:8080/aop/csdn,观察一下控制台的输出信息:
1 2 3 4 5 ====doBefore 方法进入了==== 即将执行方法为: testAop,属于com.itcodai.mutest.AopController包 用户请求的 url 为:http: ==== doAfter 方法进入了==== 方法 testAop 已经执行完
从打印出来的 Log
中可以看出程序执行的逻辑与顺序,可以很直观的掌握 @Before
和 @After
两个注解的实际作用。
3.5 @AfterReturning @AfterReturning
注解和 @After
有些类似,区别在于 @AfterReturning
注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Aspect @Component @Slf4j public class LogAspectHandler { @AfterReturning(pointcut = "pointCut()", returning = "result") public void doAfterReturning (JoinPoint joinPoint, Object result) { Signature signature = joinPoint.getSignature(); String classMethod = signature.getName(); log.info("方法{}执行完毕,返回参数为:{}" , classMethod, result); log.info("对返回参数进行业务上的增强:{}" , result + "增强版" ); } }
需要注意的是,在 @AfterReturning
注解 中,属性 returning
的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning
方法中可以对返回值进行增强,可以根据业务需要做相应的封装。我们重启一下服务,再测试一下:
1 2 方法 testAop 执行完毕,返回参数为:Hello CSDN 对返回参数进行业务上的增强:Hello CSDN 增强版
3.6 @AfterThrowing 当被切方法执行过程中抛出异常时,会进入 @AfterThrowing
注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing
属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Aspect @Component @Slf4j public class LogAspectHandler { @AfterThrowing(pointcut = "pointCut()", throwing = "ex") public void afterThrowing (JoinPoint joinPoint, Throwable ex) { Signature signature = joinPoint.getSignature(); String method = signature.getName(); log.info("执行方法{}出错,异常为:{}" , method, ex); } }
以上就是AOP的全部内容。通过几个例子就可以感受到,AOP的便捷之处。欢迎大家指出文中不足之处,喜欢的朋友来个一键三连,听说这么做的人offer拿到手软( •̀ ω •́ )y
Spring Boot 五种热部署方式 1、模板热部署 2、使用调试模式Debug实现热部署 3、spring-boot-devtools 4、Spring Loaded 5、JRebel 1、模板热部署 在 Spring Boot 中,模板引擎的页面默认是开启缓存的,如果修改了页面的内容,则刷新页面是得不到修改后的页面的,因此我们可以在application.properties中关闭模版引擎的缓存,如下:
Thymeleaf的配置:
spring.thymeleaf.cache=false
FreeMarker的配置:
spring.freemarker.cache=false
Groovy的配置:
spring.groovy.template.cache=false
Velocity的配置:
spring.velocity.cache=false
2、使用调试模式Debug实现热部署 此种方式为最简单最快速的一种热部署方式,运行系统时使用Debug模式,无需装任何插件即可,但是无发对配置文件,方法名称改变,增加类及方法进行热部署,使用范围有限。
在 Spring Boot 项目中添加 spring-boot-devtools依赖即可实现页面和代码的热部署。
如下:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > </dependency >
此种方式的特点是作用范围广,系统的任何变动包括配置文件修改、方法名称变化都能覆盖,但是后遗症也非常明显,它是采用文件变化后重启的策略来实现了,主要是节省了我们手动点击重启的时间,提高了实效性,在体验上会稍差。
spring-boot-devtools 默认关闭了模版缓存,如果使用这种方式不用单独配置关闭模版缓存。
4、Spring Loaded 此种方式与Debug模式类似,适用范围有限,但是不依赖于Debug模式启动,通过Spring Loaded库文件启动,即可在正常模式下进行实时热部署。此种需要在 run confrgration 中进行配置。
5、JRebel Jrebel是Java开发最好的热部署工具,对 Spring Boot 提供了极佳的支持,JRebel为收费软件,试用期14天。,可直接通过插件安装。
JRebel破解方式
服务器地址监听服务器地址:
http://jrebel.cicoding.cn
GUID生成器:
http://jrebel.cicoding.cn/guid
监听配置格式:
http://jrebel.cicoding.cn/GUID
生成的如下:
http://jrebel.cicoding.cn/4B068EB5-0941-4645-1E98-FC077D530A61
打开IDEA中Settings如下:
找到File -> Settings -> JRebel & XRebel 点击Chanage license,填写URL和邮箱地址
点击Chanage license!
就监听成功激活成功了!鼓掌!
然后就可以启动使用了!
基于SpringBoot,来实现MySQL读写分离技术 前言 首先思考一个问题:在高并发的场景中,关于数据库都有哪些优化的手段?常用的有以下的实现方法:读写分离、加缓存、主从架构集群、分库分表等,在互联网应用中,大部分都是读多写少 的场景,设置两个库,主库和读库。
主库的职能是负责写,从库主要是负责读,可以建立读库集群,通过读写职能在数据源上的隔离达到减少读写冲突、 释压数据库负载 、保护数据库的目的 。在实际的使用中,凡是涉及到写的部分直接切换到主库,读的部分直接切换到读库,这就是典型的读写分离技术。本篇博文将聚焦读写分离,探讨如何实现它。
目录 一: 主从数据源的配置 二: 数据源路由的配置 三:数据源上下文环境 四:切换注解和 Aop 配置 五:用法以及测试 六:总结
主从同步的局限性 :这里分为主数据库和从数据库,主数据库和从数据库保持数据库结构的一致,主库负责写,当写入数据的时候,会自动同步数据到从数据库;从数据库负责读,当读请求来的时候,直接从读库读取数据,主数据库会自动进行数据复制到从数据库中。不过本篇博客不介绍这部分配置的知识,因为它更偏运维工作一点。
这里涉及到一个问题:主从复制的延迟问题,当写入到主数据库的过程中,突然来了一个读请求,而此时数据还没有完全同步,就会出现读请求的数据读不到或者读出的数据比原始值少的情况。具体的解决方法最简单的就是将读请求暂时指向主库,但是同时也失去了主从分离的部分意义。也就是说在严格意义上的数据一致性场景中,读写分离并非是完全适合的,注意更新的时效性是读写分离使用的缺点。
好了,这部分只是了解,接下来我们看下具体如何通过 java 代码来实现读写分离:
该项目需要引入如下依赖:springBoot、spring-aop、spring-jdbc、aspectjweaver 等
一: 主从数据源的配置 我们需要配置主从数据库,主从数据库的配置一般都是写在配置文件里面。通过@ConfigurationProperties 注解,可以将配置文件(一般命名为:application.Properties)里的属性映射到具体的类属性上,从而读取到写入的值注入到具体的代码配置中,按照习惯大于约定的原则,主库我们都是注为 master,从库注为 slave。
本项目采用了阿里的 druid 数据库连接池,使用 build 建造者模式创建 DataSource 对象,DataSource 就是代码层面抽象出来的数据源,接着需要配置 sessionFactory、sqlTemplate、事务管理器等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 @Configuration @MapperScan(basePackages = "com.wyq.mysqlreadwriteseparate.mapper", sqlSessionTemplateRef = "sqlTemplate") public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource master () { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slaver () { return DruidDataSourceBuilder.create().build(); } @Bean public DataSourceRouter dynamicDB (@Qualifier("master") DataSource masterDataSource, @Autowired(required = false) @Qualifier("slaver") DataSource slaveDataSource) { DataSourceRouter dynamicDataSource = new DataSourceRouter (); Map<Object, Object> targetDataSources = new HashMap <>(); targetDataSources.put(DataSourceEnum.MASTER.getDataSourceName(), masterDataSource); if (slaveDataSource != null ) { targetDataSources.put(DataSourceEnum.SLAVE.getDataSourceName(), slaveDataSource); } dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(masterDataSource); return dynamicDataSource; } @Bean public SqlSessionFactory sessionFactory (@Qualifier("dynamicDB") DataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean (); bean.setMapperLocations( new PathMatchingResourcePatternResolver ().getResources("classpath*:mapper/*Mapper.xml" )); bean.setDataSource(dynamicDataSource); return bean.getObject(); } @Bean public SqlSessionTemplate sqlTemplate (@Qualifier("sessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate (sqlSessionFactory); } @Bean(name = "dataSourceTx") public DataSourceTransactionManager dataSourceTransactionManager (@Qualifier("dynamicDB") DataSource dynamicDataSource) { DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager (); dataSourceTransactionManager.setDataSource(dynamicDataSource); return dataSourceTransactionManager; } }
二: 数据源路由的配置 路由在主从分离是非常重要的,基本是读写切换的核心。Spring 提供了 AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,作用就是在执行查询之前,设置使用的数据源,实现动态路由的数据源,在每次数据库查询操作前执行它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
为了能有一个全局的数据源管理器,此时我们需要引入 DataSourceContextHolder 这个数据库上下文管理器,可以理解为全局的变量,随时可取(见下面详细介绍),它的主要作用就是保存当前的数据源;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class DataSourceRouter extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey () { return DataSourceContextHolder.get(); } }
三:数据源上下文环境 数据源上下文保存器,便于程序中可以随时取到当前的数据源,它主要利用 ThreadLocal 封装,因为 ThreadLocal 是线程隔离的,天然具有线程安全的优势。这里暴露了 set 和 get、clear 方法,set 方法用于赋值当前的数据源名,get 方法用于获取当前的数据源名称,clear 方法用于清除 ThreadLocal 中的内容,因为 ThreadLocal 的 key 是 weakReference 是有内存泄漏风险的,通过 remove 方法防止内存泄漏;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class DataSourceContextHolder { private static final ThreadLocal<String> context = new ThreadLocal <>(); public static void set (String datasourceType) { context.set(datasourceType); } public static String get () { return context.get(); } public static void clear () { context.remove(); } }
四:切换注解和 Aop 配置 首先我们来定义一个@DataSourceSwitcher 注解,拥有两个属性 ① 当前的数据源 ② 是否清除当前的数据源,并且只能放在方法上,(不可以放在类上,也没必要放在类上,因为我们在进行数据源切换的时候肯定是方法操作),该注解的主要作用就是进行数据源的切换,在 dao 层进行操作数据库的时候,可以在方法上注明表示的是当前使用哪个数据源;
@DataSourceSwitcher 注解的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface DataSourceSwitcher { DataSourceEnum value () default DataSourceEnum.MASTER; boolean clear () default true ; }
DataSourceAop 配置
为了赋予@DataSourceSwitcher 注解能够切换数据源的能力,我们需要使用 AOP,然后使用@Aroud 注解找到方法上有@DataSourceSwitcher.class 的方法,然后取注解上配置的数据源的值,设置到 DataSourceContextHolder 中,就实现了将当前方法上配置的数据源注入到全局作用域当中;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Slf4j @Aspect @Order(value = 1) @Component public class DataSourceContextAop { @Around("@annotation(com.wyq.mysqlreadwriteseparate.annotation.DataSourceSwitcher)") public Object setDynamicDataSource (ProceedingJoinPoint pjp) throws Throwable { boolean clear = false ; try { Method method = this .getMethod(pjp); DataSourceSwitcher dataSourceSwitcher = method.getAnnotation(DataSourceSwitcher.class); clear = dataSourceSwitcher.clear(); DataSourceContextHolder.set(dataSourceSwitcher.value().getDataSourceName()); log.info("数据源切换至:{}" , dataSourceSwitcher.value().getDataSourceName()); return pjp.proceed(); } finally { if (clear) { DataSourceContextHolder.clear(); } } } private Method getMethod (JoinPoint pjp) { MethodSignature signature = (MethodSignature) pjp.getSignature(); return signature.getMethod(); } }
五:用法以及测试 在配置好了读写分离之后,就可以在代码中使用了,一般而言我们使用在 service 层或者 dao 层,在需要查询的方法上添加@DataSourceSwitcher(DataSourceEnum.SLAVE),它表示该方法下所有的操作都走的是读库;在需要 update 或者 insert 的时候使用@DataSourceSwitcher(DataSourceEnum.MASTER)表示接下来将会走写库。
其实还有一种更为自动的写法,可以根据方法的前缀来配置 AOP 自动切换数据源,比如 update、insert、fresh 等前缀的方法名一律自动设置为写库,select、get、query 等前缀的方法名一律配置为读库,这是一种更为自动的配置写法。缺点就是方法名需要按照 aop 配置的严格来定义,否则就会失效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Service public class OrderService { @Resource private OrderMapper orderMapper; @DataSourceSwitcher(DataSourceEnum.SLAVE) public List<Order> getOrder (String orderId) { return orderMapper.listOrders(orderId); } @DataSourceSwitcher(DataSourceEnum.MASTER) public List<Order> insertOrder (Long orderId) { Order order = new Order (); order.setOrderId(orderId); return orderMapper.saveOrder(order); } }
六:总结
上面是基本流程简图,本篇博客介绍了如何实现数据库读写分离,注意读写分离的核心点就是数据路由,需要继承 AbstractRoutingDataSource,复写它的 determineCurrentLookupKey 方法,同时需要注意全局的上下文管理器 DataSourceContextHolder,它是保存数据源上下文的主要类,也是路由方法中寻找的数据源取值,相当于数据源的中转站.再结合 jdbc-Template 的底层去创建和管理数据源、事务等,我们的数据库读写分离就完美实现了。
Spring Boot 注解大全 一、注解 (annotations) 列表 @SpringBootApplication :
包含了 @ComponentScan、@Configuration 和 @EnableAutoConfiguration 注解。
其中 @ComponentScan 让 spring Boot 扫描到 Configuration 类并把它加入到程序上下文。
@Configuration 等同于 spring 的 XML 配置文件;使用 Java 代码可以检查类型安全。
**@EnableAutoConfiguration ** 自动配置。
**@ComponentScan ** 组件扫描,可自动发现和装配一些 Bean。
@Component 可配合 CommandLineRunner 使用,在程序启动后执行一些基础任务。
@RestController 注解是 @Controller 和 @ResponseBody 的合集, 表示这是个控制器 bean, 并且是将函数的返回值直 接填入 HTTP 响应体中, 是 REST 风格的控制器。
@Autowired 自动导入。
@PathVariable 获取参数。
@JsonBackReference 解决嵌套外链问题。
@RepositoryRestResourcepublic 配合 spring-boot-starter-data-rest 使用。
二、注解 (annotations) 详解 @SpringBootApplication :申明让 spring boot 自动给程序进行必要的配置,这个配置等同于:@Configuration ,@EnableAutoConfiguration 和 @ComponentScan 三个配置。
1 2 3 4 5 6 7 8 9 10 package com.example.myproject;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication public class Application { public static void main (String[] args) { SpringApplication.run(Application.class, args); } }
@ResponseBody :表示该方法的返回结果直接写入 HTTP response body 中,一般在异步获取数据时使用,用于构建 RESTful 的 api。
在使用 @RequestMapping 后,返回值通常解析为跳转路径,加上 @responsebody 后返回结果不会被解析为跳转路径,而是直接写入 HTTP response body 中。
比如异步获取 json 数据,加上 @responsebody 后,会直接返回 json 数据。
该注解一般会配合 @RequestMapping 一起使用。示例代码:
1 2 3 4 5 @RequestMapping(“/test”) @ResponseBody public String test () { return ”ok”; }
@Controller :用于定义控制器类,在 spring 项目中由控制器负责将用户发来的 URL 请求转发到对应的服务接口(service 层)
一般这个注解在类中,通常方法需要配合注解 @RequestMapping。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Controller @RequestMapping(“/demoInfo”) publicclass DemoController { @Autowired private DemoInfoService demoInfoService; @RequestMapping("/hello") public String hello (Map map) { System.out.println("DemoController.hello()" ); map.put("hello" ,"from TemplateController.helloHtml" ); return "/hello" ; } }
@RestController :用于标注控制层组件 (如 struts 中的 action),@ResponseBody 和 @Controller 的合集。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kfit.demo.web;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping(“/demoInfo2”) publicclass DemoController2 { @RequestMapping("/test") public String test () { return "ok" ; } }
@RequestMapping :提供路由信息,负责 URL 到 Controller 中的具体函数的映射。
@EnableAutoConfiguration :Spring Boot 自动配置(auto-configuration):尝试根据你添加的 jar 依赖自动配置你的 Spring 应用。
例如,如果你的 classpath 下存在 HSQLDB,并且你没有手动配置任何数据库连接 beans,那么我们将自动配置一个内存型(in-memory)数据库”。
你可以将 @EnableAutoConfiguration 或者 @SpringBootApplication 注解添加到一个 @Configuration 类上来选择自动配置。
如果发现应用了你不想要的特定自动配置类,你可以使用 @EnableAutoConfiguration 注解的排除属性来禁用它们。
@ComponentScan :表示将该类自动发现扫描组件。
个人理解相当于,如果扫描到有 @Component、@Controller、@Service 等这些注解的类,并注册为 Bean,可以自动收集所有的 Spring 组件,包括 @Configuration 类。
我们经常使用 @ComponentScan 注解搜索 beans,并结合 @Autowired 注解导入。可以自动收集所有的 Spring 组件,包括 @Configuration 类。
如果没有配置的话,Spring Boot 会扫描启动类所在包下以及子包下的使用了 @Service,@Repository 等注解的类。
@Configuration :相当于传统的 xml 配置文件,如果有些第三方库需要用到 xml 文件,建议仍然通过 @Configuration 类作为项目的配置主类——可以使用 @ImportResource 注解加载 xml 配置文件。
@Import :用来导入其他配置类。
@ImportResource :用来加载 xml 配置文件。
@Autowired :自动导入依赖的 bean
@Service :一般用于修饰 service 层的组件
@Repository :使用 @Repository 注解可以确保 DAO 或者 repositories 提供异常转译,这个注解修饰的 DAO 或者 repositories 类会被 ComponetScan 发现并配置,同时也不需要为它们提供 XML 配置项。
@Bean :用 @Bean 标注方法等价于 XML 中配置的 bean。
@Value :注入 Spring boot application.properties 配置的属性的值。示例代码:
1 2 @Value(value = “#{message}”) private String message;
@Inject :等价于默认的 @Autowired,只是没有 required 属性;
@Component :泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。
@Bean :相当于 XML 中的, 放在方法的上面,而不是类,意思是产生一个 bean, 并交给 spring 管理。
@AutoWired :自动导入依赖的 bean。byType 方式。把配置好的 Bean 拿来用,完成属性、方法的组装,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。当加上(required=false)时,就算找不到 bean 也不报错。
@Qualifier :当有多个同一类型的 Bean 时,可以用 @Qualifier(“name”) 来指定。与 @Autowired 配合使用。@Qualifier 限定描述符除了能根据名字进行注入,但能进行更细粒度的控制如何选择候选者,具体使用方式如下:
1 2 3 @Autowired @Qualifier(value = “demoInfoService”) private DemoInfoService demoInfoService;
@Resource (name=”name”,type=”type”):没有括号内内容的话,默认 byName。与 @Autowired 干类似的事。
三、JPA 注解 @Entity :@Table(name=”“):表明这是一个实体类。一般用于 jpa 这两个注解一般一块使用,但是如果表名和实体类名相同的话,@Table 可以省略
@MappedSuperClass : 用在确定是父类的 entity 上。父类的属性子类可以继承。
@NoRepositoryBean : 一般用作父类的 repository,有这个注解,spring 不会去实例化该 repository。
@Column :如果字段名与列名相同,则可以省略。
@Id :表示该属性为主键。
@GeneratedValue (strategy=GenerationType.SEQUENCE,generator= “repair_seq”):表示主键生成策略是 sequence(可以为 Auto、IDENTITY、native 等,Auto 表示可在多个数据库间切换),指定 sequence 的名字是 repair_seq。
@SequenceGeneretor (name = “repair_seq”, sequenceName = “seq_repair”, allocationSize = 1):name 为 sequence 的名称,以便使用,sequenceName 为数据库的 sequence 名称,两个名称可以一致。
@Transient :表示该属性并非一个到数据库表的字段的映射, ORM 框架将忽略该属性。
如果一个属性并非数据库表的字段映射, 就务必将其标示为 @Transient, 否则, ORM 框架默认其注解为 @Basic。@Basic(fetch=FetchType.LAZY):标记可以指定实体属性的加载方式
@JsonIgnore :作用是 json 序列化时将 Java bean 中的一些属性忽略掉, 序列化和反序列化都受影响。
@JoinColumn (name=”loginId”): 一对一:本表中指向另一个表的外键。一对多:另一个表指向本表的外键。
@OneToOne、@OneToMany、@ManyToOne :对应 hibernate 配置文件中的一对一,一对多,多对一。
四、springMVC 相关注解 @RequestMapping :@RequestMapping(“/path”)表示该控制器处理所有 “/path” 的 UR L 请求。
RequestMapping 是一个用来处理请求地址映射的注解,可用于类或方法上。
用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。该注解有六个属性:
params : 指定 request 中必须包含某些参数值是,才让该方法处理。
headers : 指定 request 中必须包含某些指定的 header 值,才能让该方法处理请求。
value : 指定请求的实际地址,指定的地址可以是 URI Template 模式
method : 指定请求的 method 类型, GET、POST、PUT、DELETE 等
consumes : 指定处理请求的提交内容类型(Content-Type),如 application/json,text/html;
produces : 指定返回的内容类型,仅当 request 请求头中的 (Accept) 类型中包含该指定类型才返回
@RequestParam :用在方法的参数前面。 @RequestParam String a =request.getParameter(“a”)。
@PathVariable : 路径变量。如
1 2 3 4 RequestMapping(“user/get/mac/{macAddress}”) public String getByMacAddress (@PathVariable String macAddress) { }
参数与大括号里的名字一样要相同。
五、全局异常处理 @ControllerAdvice :包含 @Component。可以被扫描到。统一处理异常。
@ExceptionHandler (Exception.class):用在方法上面表示遇到这个异常就执行以下方法。
SpringBoot第三方jar包打包问题 一、操作步骤 1、首先项目引入jar包
引入jar包
2、pom文件中引入jar
1 2 3 4 5 6 7 8 <dependency > <groupId > com.topsoft</groupId > <artifactId > cms-pak</artifactId > <version > 0.0.1-SNAPSHOT</version > <scope > system</scope > <systemPath > ${project.basedir}/src/main/resources/lib/cms-pak.jar</systemPath > </dependency >
3、plugin配置 加上配置,将本地依赖包打进jar里
1 2 3 <configuration > <includeSystemScope > true</includeSystemScope > </configuration >
1 2 3 4 5 6 7 8 9 10 11 <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <configuration > <includeSystemScope > true</includeSystemScope > </configuration > </plugin > <plugins > </build >
二、补充 可以通过jd-gui工具查看jar源码
若依框架二级域名配置 一、前言 若依官网项目部署手册,前端部署到80端口一级域名下的。一级域名官网占用,这时就需要配置二级域名。
二、ruoyi-ui 前端vue配置 2.1 配置vue.config.js 修改publicPath
1 2 3 4 5 6 7 8 9 10 11 12 13 14 module .exports = { publicPath : process.env .NODE_ENV === "production" ? "/admin/" : "/admin/" , outputDir : 'dist' , assetsDir : 'static' , lintOnSave : process.env .NODE_ENV === 'development' , productionSourceMap : false ,
2.2 配置router 修改router目录下的index.js文件 添加 base:‘admin’
1 2 3 4 5 6 export default new Router ({ base :'admin' , mode : 'history' , scrollBehavior : () => ({ y : 0 }), routes : constantRoutes })
2.3 配置登出地址 修改/src/layout/componets/Navbar.vue文件里的logout()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 methods : { toggleSideBar ( ) { this .$store .dispatch ('app/toggleSideBar' ) }, async logout ( ) { this .$confirm('确定注销并退出系统吗?' , '提示' , { confirmButtonText : '确定' , cancelButtonText : '取消' , type : 'warning' }).then (() => { this .$store .dispatch ('LogOut' ).then (() => { location.href = '/admin' }) }).catch (() => {}); } }
三、Nginx配置 一级域名一般部署的是公司官网website-dist,二级域名部署后台项目ruoyi-dist。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 server { listen 80 ; server_name localhost; charset utf-8 ; location / { root /root/website/website-dist; try_files $uri $uri / /index.html; index index.html index.htm; } location /admin { alias /home/ruoyi/ruoyi-dist; try_files $uri $uri / /index.html; index index.html index.htm; } location /prod-api/ { proxy_set_header Host $http_host ; proxy_set_header X-Real-IP $remote_addr ; proxy_set_header REMOTE-HOST $remote_addr ; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ; proxy_pass http://127.0.0.1:9004/; } }
四,常见报错 安装页面部署,使用nginx 在前面做代理,但是无法打开页面,系统页面也报错:Error: Cannot find module ‘@/views/system/role/index
修改 src/store/modules/permission.js
1 2 3 4 5 6 7 8 9 10 export const loadView = (view ) => { if (process.env .NODE_ENV === 'development' ) { return (resolve ) => require ([`@/views/${view} ` ], resolve) } else { return (resolve ) => require ([`@/views/${view} ` ], resolve); } }