宿主机创建映射文件目录，分别是/home/mysql/conf，/home/mysql/data、/home/mysql/logsconf目录下创建my.cnf文件[client] default-character-set = utf8mb4 [mysqld] pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock datadir = /var/lib/mysql secure-file-priv= NULL # Custom config should go here # 字符集 character_set_server=utf8mb4 collation-server=utf8mb4_unicode_ci # 创建新表时将使用的默认存储引擎 default-storage-engine=INNODB # 是否对sql语句大小写敏感，0:大小写敏感,1:忽略大小写区别。，只能在初始化服务器时配置。禁止在服务器初始化后更改 # 设置为2时，表名和数据库名按声明存储，但以小写形式进行比较 lower_case_table_names = 2 # 最大连接数 max_connections = 1000 # Innodb缓存池大小 innodb_buffer_pool_size = 4G # 表文件描述符的缓存大小 table_open_cache_instances=1 table_open_cache=2000 table_definition_cache=2000 !includedir /etc/mysql/conf.d/在conf、data、logs同层目录下，创建start.shdocker run --restart=always \ --name mysql \ --privileged=true \ -d -p 3306:3306 \ -v /home/mysql/conf/my.cnf:/etc/mysql/my.cnf \ -v /home/mysql/logs:/logs \ -v /home/mysql/data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=Hanyuan1230@... \ mysql:8.0.35拉取docker的镜像文件 docker pull mysql:8.0.35sh start.sh命令执行脚本代码，创建mysql的docker容器进入mysql容器，docker exec -it mysql bash登录mysql，mysql -u root -p此时root用户外界还不能使用，要给root分配权限。第一行命令：grant all PRIVILEGES on . to root@'%' WITH GRANT OPTION;第二行命令：use mysql第三行命令：update user set host='%' where user='root';至此，外界就可以用root连接到mysql了
授权，也叫访问控制，即在应用中控制谁能访问哪些资源（如访问页面/编辑数据/页面操作等）。在授权中需了解的几个关键对象：主体（Subject）、资源（Resource）、权限（Permission）、角色（Role） 主体主体，即访问应用的用户，在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。资源在应用中用户可以访问的任何东西，比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。权限安全策略中的原子授权单位，通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源，如：访问用户列表页面查看/新增/修改/删除用户数据（即很多时候都是CRUD（增查改删）式权限控制）打印文档等等。。。如上可以看出，权限代表了用户有没有操作某个资源的权利，即反映在某个资源上的操作允不允许，不反映谁去执行这个操作。所以后续还需要把权限赋予给用户，即定义哪个用户允许在某个资源上做什么操作（权限），Shiro不会去做这件事情，而是由实现人员提供。Shiro支持粗粒度权限（如用户模块的所有权限）和细粒度权限（操作某个用户的权限，即实例级别的），后续部分介绍。角色角色代表了操作集合，可以理解为权限的集合，一般情况下我们会赋予用户角色而不是权限，即这样用户可以拥有一组权限，赋予权限时比较方便。典型的如：项目经理、技术总监、CTO、开发工程师等都是角色，不同的角色拥有一组不同的权限。隐式角色：即直接通过角色来验证用户有没有操作权限，如在应用中CTO、技术总监、开发工程师可以使用打印机，假设某天不允许开发工程师使用打印机，此时需要从应用中删除相应代码；再如在应用中CTO、技术总监可以查看用户、查看权限；突然有一天不允许技术总监查看用户、查看权限了，需要在相关代码中把技术总监角色从判断逻辑中删除掉；即粒度是以角色为单位进行访问控制的，粒度较粗；如果进行修改可能造成多处代码修改。显示角色：在程序中通过权限控制谁能访问某个资源，角色聚合一组权限集合；这样假设哪个角色不能访问某个资源，只需要从角色代表的权限集合中移除即可；无须修改多处代码；即粒度是以资源/实例为单位的；粒度较细。请google搜索“RBAC”和“RBAC新解”分别了解“基于角色的访问控制”“基于资源的访问控制(Resource-Based Access Control)”。授权方式Shiro支持三种方式的授权：编程式：通过写if/else授权代码块完成：Subject subject = SecurityUtils.getSubject(); if(subject.hasRole(“admin”)) { //有权限 } else { //无权限 }注解式：通过在执行的Java方法上放置相应的注解完成：@RequiresRoles("admin") public void hello() { //有权限 } 没有权限将抛出相应的异常；JSP/GSP标签：在JSP/GSP页面通过相应的标签完成：<shiro:hasRole name="admin"> <!— 有权限 —></shiro:hasRole> 后续部分将详细介绍如何使用。 授权基于角色的访问控制（隐式角色）1、在ini配置文件配置用户拥有的角色（shiro-role.ini）[users] zhang=123,role1,role2 wang=123,role1规则即：“用户名=密码,角色1，角色2”，如果需要在应用中判断用户是否有相应角色，就需要在相应的Realm中返回角色信息，也就是说Shiro不负责维护用户-角色信息，需要应用提供，Shiro只是提供相应的接口方便验证，后续会介绍如何动态的获取用户角色。2、测试用例（com.github.zhangkaitao.shiro.chapter3.RoleTest）@Test public void testHasRole() { login("classpath:shiro-role.ini", "zhang", "123"); //判断拥有角色：role1 Assert.assertTrue(subject().hasRole("role1")); //判断拥有角色：role1 and role2 Assert.assertTrue(subject().hasAllRoles(Arrays.asList("role1", "role2"))); //判断拥有角色：role1 and role2 and !role3 boolean[] result = subject().hasRoles(Arrays.asList("role1", "role2", "role3")); Assert.assertEquals(true, result[0]); Assert.assertEquals(true, result[1]); Assert.assertEquals(false, result[2]); } Shiro提供了hasRole/hasRole用于判断用户是否拥有某个角色/某些权限；但是没有提供如hashAnyRole用于判断是否有某些权限中的某一个。@Test(expected = UnauthorizedException.class) public void testCheckRole() { login("classpath:shiro-role.ini", "zhang", "123"); //断言拥有角色：role1 subject().checkRole("role1"); //断言拥有角色：role1 and role3 失败抛出异常 subject().checkRoles("role1", "role3"); }Shiro提供的checkRole/checkRoles和hasRole/hasAllRoles不同的地方是它在判断为假的情况下会抛出UnauthorizedException异常。到此基于角色的访问控制（即隐式角色）就完成了，这种方式的缺点就是如果很多地方进行了角色判断，但是有一天不需要了那么就需要修改相应代码把所有相关的地方进行删除；这就是粗粒度造成的问题。基于资源的访问控制（显示角色）1、在ini配置文件配置用户拥有的角色及角色-权限关系（shiro-permission.ini）[users] zhang=123,role1,role2 wang=123,role1 [roles] role1=user:create,user:update role2=user:create,user:delete 规则：“用户名=密码，角色1，角色2”“角色=权限1，权限2”，即首先根据用户名找到角色，然后根据角色再找到权限；即角色是权限集合；Shiro同样不进行权限的维护，需要我们通过Realm返回相应的权限信息。只需要维护“用户——角色”之间的关系即可。2、测试用例（com.github.zhangkaitao.shiro.chapter3.PermissionTest）@Test public void testIsPermitted() { login("classpath:shiro-permission.ini", "zhang", "123"); //判断拥有权限：user:create Assert.assertTrue(subject().isPermitted("user:create")); //判断拥有权限：user:update and user:delete Assert.assertTrue(subject().isPermittedAll("user:update", "user:delete")); //判断没有权限：user:view Assert.assertFalse(subject().isPermitted("user:view")); } Shiro提供了isPermitted和isPermittedAll用于判断用户是否拥有某个权限或所有权限，也没有提供如isPermittedAny用于判断拥有某一个权限的接口。@Test(expected = UnauthorizedExceptionclass) public void testCheckPermission () { login("classpath:shiro-permissionini", "zhang", "123"); //断言拥有权限：user:create subject().checkPermission("user:create"); //断言拥有权限：user:delete and user:update subject().checkPermissions("user:delete", "user:update"); //断言拥有权限：user:view 失败抛出异常 subject().checkPermissions("user:view"); } 但是失败的情况下会抛出UnauthorizedException异常。到此基于资源的访问控制（显示角色）就完成了，也可以叫基于权限的访问控制，这种方式的一般规则是“资源标识符：操作”，即是资源级别的粒度；这种方式的好处就是如果要修改基本都是一个资源级别的修改，不会对其他模块代码产生影响，粒度小。但是实现起来可能稍微复杂点，需要维护“用户——角色，角色——权限（资源：操作）”之间的关系。 Permission字符串通配符权限规则：“资源标识符：操作：对象实例ID” 即对哪个资源的哪个实例可以进行什么操作。其默认支持通配符权限字符串，“:”表示资源/操作/实例的分割；“,”表示操作的分割；“*”表示任意资源/操作/实例。1、单个资源单个权限subject().checkPermissions("system:user:update");用户拥有资源“system:user”的“update”权限。2、单个资源多个权限ini配置文件role41=system:user:update,system:user:delete然后通过如下代码判断subject().checkPermissions("system:user:update", "system:user:delete");用户拥有资源“system:user”的“update”和“delete”权限。如上可以简写成：ini配置（表示角色4拥有system:user资源的update和delete权限）role42="system:user:update,delete"接着可以通过如下代码判断subject().checkPermissions("system:user:update,delete");通过“system:user:update,delete”验证"system:user:update, system:user:delete"是没问题的，但是反过来是规则不成立。3、单个资源全部权限ini配置role51="system:user:create,update,delete,view"然后通过如下代码判断subject().checkPermissions("system:user:create,delete,update:view");用户拥有资源“system:user”的“create”、“update”、“delete”和“view”所有权限。如上可以简写成：ini配置文件（表示角色5拥有system:user的所有权限）role52=system:user:*也可以简写为（推荐上边的写法）：role53=system:user然后通过如下代码判断subject().checkPermissions("system:user:*"); subject().checkPermissions("system:user"); 通过“system:user:*”验证“system:user:create,delete,update:view”可以，但是反过来是不成立的。4、所有资源全部权限ini配置role61=*:view然后通过如下代码判断subject().checkPermissions("user:view"); 用户拥有所有资源的“view”所有权限。假设判断的权限是“"system:user:view”，那么需要“role5=::view”这样写才行。5、实例级别的权限5.1、单个实例单个权限ini配置role71=user:view:1对资源user的1实例拥有view权限。然后通过如下代码判断 subject().checkPermissions("user:view:1");5.2、单个实例多个权限ini配置 role72="user:update,delete:1"对资源user的1实例拥有update、delete权限。然后通过如下代码判断subject().checkPermissions("user:delete,update:1"); subject().checkPermissions("user:update:1", "user:delete:1");5.3、单个实例所有权限ini配置 role73=user:*:1对资源user的1实例拥有所有权限。然后通过如下代码判断 subject().checkPermissions("user:update:1", "user:delete:1", "user:view:1");5.4、所有实例单个权限ini配置 role74=user:auth:*对资源user的1实例拥有所有权限。然后通过如下代码判断 subject().checkPermissions("user:auth:1", "user:auth:2");5.5、所有实例所有权限ini配置 role75=user::对资源user的1实例拥有所有权限。然后通过如下代码判断 subject().checkPermissions("user:view:1", "user:auth:2");6、Shiro对权限字符串缺失部分的处理如“user:view”等价于“user:view:”；而“organization”等价于“organization:”或者“organization::”。可以这么理解，这种方式实现了前缀匹配。另外如“user:”可以匹配如“user:delete”、“user:delete”可以匹配如“user:delete:1”、“user::1”可以匹配如“user:view:1”、“user”可以匹配“user:view”或“user:view:1”等。即可以匹配所有，不加可以进行前缀匹配；但是如“:view”不能匹配“system:user:view”，需要使用“::view”，即后缀匹配必须指定前缀（多个冒号就需要多个来匹配）。7、WildcardPermission如下两种方式是等价的： subject().checkPermission("menu:view:1"); subject().checkPermission(new WildcardPermission("menu:view:1"));因此没什么必要的话使用字符串更方便。8、性能问题通配符匹配方式比字符串相等匹配来说是更复杂的，因此需要花费更长时间，但是一般系统的权限不会太多，且可以配合缓存来提供其性能，如果这样性能还达不到要求我们可以实现位操作算法实现性能更好的权限匹配。另外实例级别的权限验证如果数据量太大也不建议使用，可能造成查询权限及匹配变慢。可以考虑比如在sql查询时加上权限字符串之类的方式在查询时就完成了权限匹配。 授权流程流程如下：1、首先调用Subject.isPermitted/hasRole接口，其会委托给SecurityManager，而SecurityManager接着会委托给Authorizer；2、Authorizer是真正的授权者，如果我们调用如isPermitted(“user:view”)，其首先会通过PermissionResolver把字符串转换成相应的Permission实例；3、在进行授权之前，其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限；4、Authorizer会判断Realm的角色/权限是否和传入的匹配，如果有多个Realm，会委托给ModularRealmAuthorizer进行循环判断，如果匹配如isPermitted/hasRole会返回true，否则返回false表示授权失败。ModularRealmAuthorizer进行多Realm匹配流程：1、首先检查相应的Realm是否实现了实现了Authorizer；2、如果实现了Authorizer，那么接着调用其相应的isPermitted/hasRole接口进行匹配；3、如果有一个Realm匹配那么将返回true，否则返回false。如果Realm进行授权的话，应该继承AuthorizingRealm，其流程是：1.1、如果调用hasRole*，则直接获取AuthorizationInfo.getRoles()与传入的角色比较即可；1.2、首先如果调用如isPermitted(“user:view”)，首先通过PermissionResolver将权限字符串转换成相应的Permission实例，默认使用WildcardPermissionResolver，即转换为通配符的WildcardPermission；2、通过AuthorizationInfo.getObjectPermissions()得到Permission实例集合；通过AuthorizationInfo. getStringPermissions()得到字符串集合并通过PermissionResolver解析为Permission实例；然后获取用户的角色，并通过RolePermissionResolver解析角色对应的权限集合（默认没有实现，可以自己提供）；3、接着调用Permission. implies(Permission p)逐个与传入的权限比较，如果有匹配的则返回true，否则false。 Authorizer、PermissionResolver及RolePermissionResolverAuthorizer的职责是进行授权（访问控制），是Shiro API中授权核心的入口点，其提供了相应的角色/权限判断接口，具体请参考其Javadoc。SecurityManager继承了Authorizer接口，且提供了ModularRealmAuthorizer用于多Realm时的授权匹配。PermissionResolver用于解析权限字符串到Permission实例，而RolePermissionResolver用于根据角色解析相应的权限集合。我们可以通过如下ini配置更改Authorizer实现：authorizer=org.apache.shiro.authz.ModularRealmAuthorizer securityManager.authorizer=$authorizer对于ModularRealmAuthorizer，相应的AuthorizingSecurityManager会在初始化完成后自动将相应的realm设置进去，我们也可以通过调用其setRealms()方法进行设置。对于实现自己的authorizer可以参考ModularRealmAuthorizer实现即可，在此就不提供示例了。设置ModularRealmAuthorizer的permissionResolver，其会自动设置到相应的Realm上（其实现了PermissionResolverAware接口），如：permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver authorizer.permissionResolver=$permissionResolver设置ModularRealmAuthorizer的rolePermissionResolver，其会自动设置到相应的Realm上（其实现了RolePermissionResolverAware接口），如：rolePermissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.MyRolePermissionResolver authorizer.rolePermissionResolver=$rolePermissionResolver示例1、ini配置（shiro-authorizer.ini） [main] 自定义authorizer authorizer=org.apache.shiro.authz.ModularRealmAuthorizer 自定义permissionResolverpermissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver permissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.BitAndWildPermissionResolver authorizer.permissionResolver=$permissionResolver 自定义rolePermissionResolverrolePermissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.MyRolePermissionResolver 1authorizer.rolePermissionResolver=$rolePermissionResolver securityManager.authorizer=$authorizer自定义realm 一定要放在securityManager.authorizer赋值之后（因为调用setRealms会将realms设置给authorizer，并给各个Realm设置permissionResolver和rolePermissionResolver） realm=com.github.zhangkaitao.shiro.chapter3.realm.MyRealm securityManager.realms=$realm 设置securityManager 的realms一定要放到最后，因为在调用SecurityManager.setRealms时会将realms设置给authorizer，并为各个Realm设置permissionResolver和rolePermissionResolver。另外，不能使用IniSecurityManagerFactory创建的IniRealm，因为其初始化顺序的问题可能造成后续的初始化Permission造成影响。2、定义BitAndWildPermissionResolver及BitPermissionBitPermission用于实现位移方式的权限，如规则是：权限字符串格式：+资源字符串+权限位+实例ID；以+开头中间通过+分割；权限：0 表示所有权限；1 新增（二进制：0001）、2 修改（二进制：0010）、4 删除（二进制：0100）、8 查看（二进制：1000）；如 +user+10 表示对资源user拥有修改/查看权限。public class BitPermission implements Permission { private String resourceIdentify; private int permissionBit; private String instanceId; public BitPermission(String permissionString) { String[] array = permissionString.split("\\+"); if(array.length > 1) { resourceIdentify = array[1]; } if(StringUtils.isEmpty(resourceIdentify)) { resourceIdentify = "*"; } if(array.length > 2) { permissionBit = Integer.valueOf(array[2]); } if(array.length > 3) { instanceId = array[3]; } if(StringUtils.isEmpty(instanceId)) { instanceId = "*"; } } @Override public boolean implies(Permission p) { if(!(p instanceof BitPermission)) { return false; } BitPermission other = (BitPermission) p; if(!("*".equals(this.resourceIdentify) || this.resourceIdentify.equals(other.resourceIdentify))) { return false; } if(!(this.permissionBit ==0 || (this.permissionBit & other.permissionBit) != 0)) { return false; } if(!("*".equals(this.instanceId) || this.instanceId.equals(other.instanceId))) { return false; } return true; } } Permission接口提供了boolean implies(Permission p)方法用于判断权限匹配的； public class BitAndWildPermissionResolver implements PermissionResolver { @Override public Permission resolvePermission(String permissionString) { if(permissionString.startsWith("+")) { return new BitPermission(permissionString); } return new WildcardPermission(permissionString); } } BitAndWildPermissionResolver实现了PermissionResolver接口，并根据权限字符串是否以“+”开头来解析权限字符串为BitPermission或WildcardPermission。 3、定义MyRolePermissionResolver RolePermissionResolver用于根据角色字符串来解析得到权限集合。 public class MyRolePermissionResolver implements RolePermissionResolver { @Override public Collection<Permission> resolvePermissionsInRole(String roleString) { if("role1".equals(roleString)) { return Arrays.asList((Permission)new WildcardPermission("menu:*")); } return null; } } 此处的实现很简单，如果用户拥有role1，那么就返回一个“menu:*”的权限。4、自定义Realmpublic class MyRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addRole("role1"); authorizationInfo.addRole("role2"); authorizationInfo.addObjectPermission(new BitPermission("+user1+10")); authorizationInfo.addObjectPermission(new WildcardPermission("user1:*")); authorizationInfo.addStringPermission("+user2+10"); authorizationInfo.addStringPermission("user2:*"); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //和com.github.zhangkaitao.shiro.chapter2.realm.MyRealm1. getAuthenticationInfo代码一样，省略 } } 此时我们继承AuthorizingRealm而不是实现Realm接口；推荐使用AuthorizingRealm，因为：AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)：表示获取身份验证信息；AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)：表示根据用户身份获取授权信息。这种方式的好处是当只需要身份验证时只需要获取身份验证信息而不需要获取授权信息。对于AuthenticationInfo和AuthorizationInfo请参考其Javadoc获取相关接口信息。另外我们可以使用JdbcRealm，需要做的操作如下：1、执行sql/ shiro-init-data.sql 插入相关的权限数据；2、使用shiro-jdbc-authorizer.ini配置文件，需要设置jdbcRealm.permissionsLookupEnabled为true来开启权限查询。此次还要注意就是不能把我们自定义的如“+user1+10”配置到INI配置文件，即使有IniRealm完成，因为IniRealm在new完成后就会解析这些权限字符串，默认使用了WildcardPermissionResolver完成，即此处是一个设计权限，如果采用生命周期（如使用初始化方法）的方式进行加载就可以解决我们自定义permissionResolver的问题。5、测试用例public class AuthorizerTest extends BaseTest { @Test public void testIsPermitted() { login("classpath:shiro-authorizer.ini", "zhang", "123"); //判断拥有权限：user:create Assert.assertTrue(subject().isPermitted("user1:update")); Assert.assertTrue(subject().isPermitted("user2:update")); //通过二进制位的方式表示权限 Assert.assertTrue(subject().isPermitted("+user1+2"));//新增权限 Assert.assertTrue(subject().isPermitted("+user1+8"));//查看权限 Assert.assertTrue(subject().isPermitted("+user2+10"));//新增及查看 Assert.assertFalse(subject().isPermitted("+user1+4"));//没有删除权限 Assert.assertTrue(subject().isPermitted("menu:view"));//通过MyRolePermissionResolver解析得到的权限 } }
代码层级梳理代码层级annotation层ExcelVoAttribut主要用于写@ExcelVoAttribute注解，可以更灵活的配置Excel数据表导出的一些基本配置LogDirection主要用于写@LogDirection注解，注解里包含一个字段（接口描述），通过接口添加这个注解，填写上接口的功能，当用户在访问接口时候，可以利用aop技术，把用户的操作记到日志里aspect层LogDrectionAspect主要定义了aop切点的位置和增强的方法/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Aspect @Component @Slf4j public class LogDirectionAspect { @Pointcut("@annotation(logDirection)") public void doLogDirection(LogDirection logDirection){ } @Around(value = "doLogDirection(logDirection)",argNames = "pjp,logDirection") public Object doBefore(ProceedingJoinPoint pjp,LogDirection logDirection) throws Throwable{ if (!"".equals(logDirection.direction())) { log.info(logDirection.direction()+"接口被调用"); return pjp.proceed(); } return "接口信息不规范"; } }@Pointcut定义切点，此处定义切点为使用@LogDirection的方法@Around定义增强的方法，通过around的参数ProceedingJoinPoint可以很好的控制程序能不能继续执行下一步，在权限判定发挥着比较重要的作用拓展：利用下面的这种方法，需要在xml文件中配置bean实例，当然也可以在方法中通过手动扫包的方法获得bean实例进行操作/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Aspect public class MyAdvice { /** * 配置切点 */ @Pointcut("execution(* com.soft1851.spring.web.dao..*.insert*(..))") public void pointCut() { } /** * 配置前置增强 */ @Before("MyAdvice.pointCut()") public void beforeMethod() { System.out.println("等待数据插入"); } /** * 配置后置增强 */ @AfterReturning("execution(* com.soft1851.spring.web.dao..*.*(..))") public void afterMethod(){ System.out.println("关闭数据库"); } }xml配置实例 <bean name="forumDao" class="com.soft1851.spring.web.dao.impl.ForumDaoImpl"/> <bean name="myAdvice" class="com.soft1851.spring.web.interceptor.MyAdvice"/> <aop:aspectj-autoproxy/>common层ResponseResult和ResultCode这两个类可以将响应体统一进行封装，将接口的响应体规范化，使前端获取数据也更加友好贴下代码，防止以后找不到ResponseResult类package com.soft1851.music.admin.common; import lombok.Data; import java.io.Serializable; /** * @author Johnny * @Date: 2020/4/21 19:52 * @Description: */ @Data public class ResponseResult implements Serializable { private static final long serialVersionUID = -3948389268046368059L; private Integer code; private String msg; private Object data; private ResponseResult() { } public ResponseResult(Integer code, String msg) { this.code = code; this.msg = msg; } public static ResponseResult success() { ResponseResult result = new ResponseResult(); result.setResultCode(ResultCode.SUCCESS); return result; } public static ResponseResult success(Object data) { ResponseResult result = new ResponseResult(); result.setResultCode(ResultCode.SUCCESS); result.setData(data); return result; } public static ResponseResult failure(ResultCode resultCode) { ResponseResult result = new ResponseResult(); result.setResultCode(resultCode); return result; } public static ResponseResult failure(ResultCode resultCode, Object data) { ResponseResult result = new ResponseResult(); result.setResultCode(resultCode); result.setData(data); return result; } public void setResultCode(ResultCode code) { this.code = code.code(); this.msg = code.message(); } }ResultCode类（枚举）package com.soft1851.music.admin.common; /** * @author Johnny * @Date: 2020/4/21 19:52 * @Description: */ public enum ResultCode { /* 成功状态码 */ SUCCESS(1, "成功"), /* 通用错误：10001-19999 */ PARAM_IS_INVALID(10001, "参数无效"), PARAM_IS_BLANK(10002, "参数为空"), PARAM_TYPE_BIND_ERROR(10003, "参数类型错误"), PARAM_NOT_COMPLETE(10004, "参数缺失"), HTTP_METHOD_ERROR(10005, "请求方法错误"), HTTP_METHOD_NOT_ALLOWED(10006, "请求方法不允许"), HTTP_NOT_FOUND(10007, "请求地址错误"), BOUND_STATEMENT_NOT_FOUND(10008, "Mybatis未绑定"), CONNECTION_ERROR(10009, "网络连接错误"), ARITHMETIC_ERROR(100010, "计算错误"), /* 用户错误：20001-29999*/ USER_NOT_SIGN_IN(20001, "请先登录"), USER_PASSWORD_ERROR(20002, "密码错误"), USER_ACCOUNT_ERROR(20003, "账号错误"), USER_VERIFY_CODE_ERROR(20004, "验证码错误"), USER_CODE_TIMEOUT(20005, "验证码失效"), USER_ACCOUNT_FORBIDDEN(20006, "账号已被禁用"), USER_SIGN_UP_FAIL(20007, "用户注册失败"), USER_SIGN_IN_FAIL(20008, "用户登录失败"), USER_NOT_FOUND(20009, "用户不存在"), USER_NO_AUTH(20019, "用户权限不足"), USER_TOKEN_EXPIRES(200010, "Token已过期"), /* 业务错误：30001-39999 */ SMS_ERROR(30001, "短信业务出现问题"), UPLOAD_ERROR(30002, "上传文件业务出现问题"), CAPTCHA_ERROR(30003, "验证码业务出现问题"), /* 数据错误：40001-49999 */ RESULT_CODE_DATA_NONE(50001, "数据未找到"), DATA_IS_WRONG(50002, "数据有误"), DATA_ALREADY_EXISTED(50003, "数据已存在"), DATABASE_ERROR(50004, "数据库操作异常"), /* 服务器或系统错误：50001-599999 */ SERVER_ERROR(50000, "服务器错误，请稍后重试"), SYSTEM_ERROR(40001, "系统错误，请稍后重试"), /* 接口错误：60001-69999 */ INTERFACE_INNER_INVOKE_ERROR(60001, "内部系统接口调用异常"), INTERFACE_OUTER_INVOKE_ERROR(60002, "外部系统接口调用异常"), INTERFACE_FORBID_VISIT(60003, "该接口禁止访问"), INTERFACE_ADDRESS_INVALID(60004, "接口地址无效"), INTERFACE_REQUEST_TIMEOUT(60005, "接口请求超时"), INTERFACE_EXCEED_LOAD(60006, "接口负载过高"), /* 权限错误：70001-79999 */ PERMISSION_NO_ACCESS(70001,"无访问权限"); private Integer code; private String message; ResultCode(Integer code, String message) { this.code = code; this.message = message; } public Integer code() { return this.code; } public String message() { return this.message; } public static String getMessage(String name) { for (ResultCode item : ResultCode.values()) { if (item.name().equals(name)) { return item.message; } } return name; } public static Integer getCode(String name) { for (ResultCode item : ResultCode.values()) { if (item.name().equals(name)) { return item.code; } } return null; } @Override public String toString() { return this.name(); } }config层CaptchaConfig主要是为kaptcha谷歌验证码的一些基本信息进行配置，比如字体，验证码位数等等附上一些kaptha的基本配置信息Constant描述默认值kaptcha.border图片边框，合法值：yes , noyeskaptcha.border.color边框颜色，合法值： r,g,b (and optional alpha) 或者 white,black,blue.blackkaptcha.border.thickness边框厚度，合法值：>01kaptcha.image.width图片宽200kaptcha.image.height图片高50kaptcha.producer.impl图片实现类com.google.code.kaptcha.impl.DefaultKaptchakaptcha.textproducer.impl文本实现类com.google.code.kaptcha.text.impl.DefaultTextCreatorkaptcha.textproducer.char.string文本集合，验证码值从此集合中获取abcde2345678gfynmnpwxkaptcha.textproducer.char.length验证码长度5kaptcha.textproducer.font.names字体Arial, Courierkaptcha.textproducer.font.size字体大小40px.kaptcha.textproducer.font.color字体颜色，合法值： r,g,b 或者 white,black,blue.blackkaptcha.textproducer.char.space文字间隔2kaptcha.noise.impl干扰实现类com.google.code.kaptcha.impl.DefaultNoisekaptcha.noise.color干扰 颜色，合法值： r,g,b 或者 white,black,blue.blackkaptcha.obscurificator.impl图片样式： 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpycom.google.code.kaptcha.impl.WaterRipplekaptcha.background.impl背景实现类com.google.code.kaptcha.impl.DefaultBackgroundkaptcha.background.clear.from背景颜色渐变，开始颜色light greykaptcha.background.clear.to背景颜色渐变， 结束颜色whitekaptcha.word.impl文字渲染器com.google.code.kaptcha.text.impl.DefaultWordRendererkaptcha.session.keysession keyKAPTCHA_SESSION_KEYkaptcha.session.datesession dateKAPTCHA_SESSION_DATECorsConfig主要是为跨域做一些基本的配置，学习阶段后端必备代码展示，防止以后忘记/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Configuration public class CorsConfig { @Bean public FilterRegistrationBean<CorsFilter> corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); //放行所有跨域的客户端domain config.addAllowedOrigin("*"); //允许的请求方法列表 String[] requestMethods = {"GET", "POST", "PUT", "DELETE", "OPTIONS"}; List<String> allowedRequestMethods = Arrays.asList(requestMethods); config.setAllowedMethods(allowedRequestMethods); //允许的客户端请求头列表 String[] requestHeaders = {"x-requested-with", "Content-Type", "Authorization"}; List<String> allowedHeaders = Arrays.asList(requestHeaders); config.setAllowedHeaders(allowedHeaders); //允许的响应头列表 String[] responseHeaders = {"Access-Control-Expose-Headers", "Authorization"}; List<String> allowedExposedHeaders = Arrays.asList(responseHeaders); config.setExposedHeaders(allowedExposedHeaders); source.registerCorsConfiguration("/**", config); FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source)); // 这个顺序很重要,设置在最前 bean.setOrder(0); return bean; } }MybatisPlusConfig主要是为mybatisplus做一些基本配置（如果要有分页查询数据，则这个配置文件必须要有）/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @EnableTransactionManagement @Configuration @MapperScan("com.baomidou.cloud.service.*.mapper*") public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); //设置请求的页面大于最大页操作，true调回到首页，false继续请求，默认为false paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true)); return paginationInterceptor; } }MySqlGenerator代码生成器，功能强大无比，根据数据表，一键生成各种层级，各种实体/** * @ClassName MySqlGenerator * @Description 代码生成器 * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Slf4j public class MySqlGenerator { public static void main(String[] args) { //全局策略配置 GlobalConfig config = new GlobalConfig(); //获得当前项目根路径d:/dev/SpringBoot/spring-boot-learning String projectPath = System.getProperty("user.dir"); config.setActiveRecord(true) //作者注释 .setAuthor("Johnny") //代码生成输出路径 .setOutputDir(projectPath + "/src/main/java") //覆盖已有文件，默认false .setFileOverride(true) //是否打开输出目录窗口。默认true .setOpen(false) //开启swagger2模式 //.setSwagger2(true) //开启ActiveRecord模式 .setActiveRecord(true) //mapper添加restMap .setBaseResultMap(true) //mapper添加Base_Column_List .setBaseColumnList(true) //时间类型对应策略，默认time_pack //.setDateType(DateType.TIME_PACK) //相关包中的接口和类名后缀 .setMapperName("%sMapper") .setServiceName("%sService") .setServiceImplName("%sServiceImpl"); //数据库表配置，通过该配置，可指定需要生成哪些表或者排除哪些表 StrategyConfig strategyConfig = new StrategyConfig(); //是否大写命名 strategyConfig.setCapitalMode(true) //是否跳过视图 .setSkipView(true) //数据库表映射到实体的命名策略为驼峰式 .setNaming(NamingStrategy.underline_to_camel) //生成表，可以写多个,如果不加参数，默认为所有表 .setInclude() .setEntityBuilderModel(true) .setEntityLombokModel(true) .setRestControllerStyle(true) .setEntityTableFieldAnnotationEnable(true); //包名配置 PackageConfig packageConfig = new PackageConfig(); //父包名 packageConfig.setParent("com.soft1851.music.admin") .setMapper("mapper") .setService("service") .setController("controller") .setXml("xml") .setEntity("entity"); //数据源配置 DataSourceConfig dataSourceConfig = new DataSourceConfig(); dataSourceConfig.setDbType(DbType.MYSQL) .setDriverName("com.mysql.jdbc.Driver") .setUrl("jdbc:mysql://rm-m5ee476bu350735gjeo.mysql.rds.aliyuncs.com:3306/cloud_music?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true") .setUsername("root") .setPassword("XuNiit_#"); AutoGenerator autoGenerator = new AutoGenerator(); autoGenerator.setGlobalConfig(config) .setStrategy(strategyConfig) .setDataSource(dataSourceConfig) .setTemplateEngine(new FreemarkerTemplateEngine()) .setPackageInfo(packageConfig); autoGenerator.execute(); log.info("=============代码生成成功================"); } }RedisConfig主要对RedisConfig缓存数据库进行一些基本配置/** * @author Johnny * @Date: 2020/4/21 20:45 * @Description: */ @Configuration @EnableCaching public class RedisConfig { @Bean CacheManager cacheManager(RedisConnectionFactory connectionFactory) { //初始化RedisCacheWrite RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig(); //设置默认过期时间为1天 defaultCacheConfig.entryTtl(Duration.ofDays(1)); //初始化RedisCacheManger return new RedisCacheManager(redisCacheWriter, defaultCacheConfig); } @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object,Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值（默认使用JDK的序列化方式） Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); //使用stringRedisSerializer来序列化和反序列化redis的value值 template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } @Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) { StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(); stringRedisTemplate.setConnectionFactory(factory); return stringRedisTemplate; } }WebConfig这个类中主要配置了拦截器，可以对部分页面进行统一拦截，当然这个只是统一的拦截，具体的拦截所要进行的操作，还在下文/** * @author Johnny * @Date: 2020/4/21 20:58 * @Description: */ @Configuration public class WebConfig implements WebMvcConfigurer { @Resource private LoginInterceptor loginInterceptor; @Resource private JwtInterceptor jwtInterceptor; /** * 添加拦截器配置 */ @Override public void addInterceptors(InterceptorRegistry registry) { //拦截路径可自行配置多个 可用 ，分隔开 registry.addInterceptor(loginInterceptor).addPathPatterns("/sysAdmin/login").excludePathPatterns("/**").excludePathPatterns("/static/**"); // registry.addInterceptor(jwtInterceptor).addPathPatterns("/**").excludePathPatterns("/sysAdmin/login", "/captcha","/songList/page").excludePathPatterns("/static/**"); } }controller层controller层暂时带过，后面有时间的话把controller层常用注解汇总整理下dto层目前还没有将entity、vo、dto统一放到domain里，dto里主要是用户请求接口时需要传的参数，vo主要时后端返回前端展示的参数，而entity就是我们和数据库表所吻合的类。举例：我们数据库中的用户表的字段肯定不只时账号和密码，会有一些其他字段，比如加密盐、用户基本信息等等，但是用户请求登录接口时，只需要传账号和密码就可以了，此时此刻，为了防止资源的浪费，我们就定义一个dto来接收前端传来的参数。当用户进入主界面时，查看个人信息，后端并不需要将用户id、加密盐等私密信息展示到前端，此时，同样为了节省资源，我们定义vo视图对象，将前端需要的参数放进去，再传入前端。entity层entity层主要是将数据库中的表一对一的写好，此处是直接用代码生成器生成exceptionCustomException定义全局异常处理，使用时直接throws CustomException就好/** * @author Johnny * @Date: 2020/4/22 10:48 * @Description: */ public class CustomException extends RuntimeException { protected ResultCode resultCode; public CustomException(String msg, ResultCode resultCode) { super(msg); this.resultCode = resultCode; } public ResultCode getResultCode() { return resultCode; } }GlobalException也是全局异常处理，但和上面略有区别，非手动throw，spring在遇到异常时，会自动来这个类里查找，如果已经有配置过的，就会按照配置要求输出内容/** * @author Johnny * @Date: 2020/4/30 18:45 * @Description: */ @ControllerAdvice public class GlobalException { @ExceptionHandler(ConstraintViolationException.class) ResponseResult handleConstraintViolationException(ConstraintViolationException e) { return ResponseResult.failure(ResultCode.DATA_IS_WRONG); } }JwtException同CustomException类格式一样，但这个侧重于处理Jwt模块的异常filterChannelFilter/** * @author Johnny * @Date: 2020/4/21 20:19 * @Description: */ @WebFilter(urlPatterns = "/*",filterName = "channelFilter") public class ChannelFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { ServletRequest requestWrapper = null; if (servletRequest instanceof HttpServletRequest) { requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest); } if (requestWrapper == null) { filterChain.doFilter(servletRequest,servletResponse); }else{ filterChain.doFilter(requestWrapper, servletResponse); } } @Override public void destroy() { } }LoginFilter这个类主要针对用户登录进行过滤/** * @author Johnny * @Date: 2020/4/26 10:46 * @Description: */ @Slf4j @WebFilter(urlPatterns = "/login",filterName = "LoginFilter") public class LoginFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { ServletRequest requestWrapper = null; if (servletRequest instanceof HttpServletRequest) { String url = ((HttpServletRequest) servletRequest).getRequestURI(); //判断接口是否为导入接口 if ("/resource/guide".equals(url)) { Part file = ((HttpServletRequest) servletRequest).getPart("file"); log.info("文件名：" + file); } requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest); } if (requestWrapper == null) { filterChain.doFilter(servletRequest, servletResponse); }else{ filterChain.doFilter(requestWrapper, servletResponse); } } @Override public void destroy() { } }handle层GlobalExceptionHandle定义全局异常/** * @author Johnny * @Date: 2020/4/22 10:40 * @Description: */ @RestControllerAdvice(annotations = {RestController.class, Controller.class}) @Slf4j public class GlobalExceptionHandle { //自定义异常 /** * JwtException * @param exception * @return */ @ExceptionHandler(value = {JwtException.class}) @ResponseBody public ResponseResult sendError(JwtException exception) { log.error(exception.getMessage()); return ResponseResult.failure(exception.getResultCode()); } /** * CustomException * @param exception * @return */ @ExceptionHandler(value = {CustomException.class}) @ResponseBody public ResponseResult sendError(CustomException exception) { log.error(exception.getMessage()); return ResponseResult.failure(exception.getResultCode()); } //系统级异常 /** * InvalidClassException * @param exception * @return */ @ExceptionHandler(value = {InvalidClassException.class}) @ResponseBody public ResponseResult sendError(InvalidClaimException exception) { log.error(exception.getMessage()); //返回token已经过期 return ResponseResult.failure(ResultCode.USER_TOKEN_EXPIRES); } /** * NullPointerException * @param exception * @return */ @ExceptionHandler(value = {NullPointerException.class}) @ResponseBody public ResponseResult sendError(NullPointerException exception) { log.error(exception.getMessage()); //空指针提示数据未找到 return ResponseResult.failure(ResultCode.RESULT_CODE_DATA_NONE); } /** * IOException * @param exception * @return */ @ExceptionHandler(value = {IOException.class}) @ResponseBody public ResponseResult sendError(IOException exception) { log.error(exception.getMessage()); //io异常提示验证码出现问题 return ResponseResult.failure(ResultCode.CAPTCHA_ERROR); }GlobalResponseHandle处理全局的响应头（当接口返回类型非ResponseResult类型时，可以自动转换）/** * @author Johnny * @Date: 2020/4/22 10:55 * @Description: */ @ControllerAdvice public class GlobalResponseHandle implements ResponseBodyAdvice { /** * 处理响应的具体方法 * @param returnType * @param converterType * @return */ @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof ResponseResult) { return body; }else{ return ResponseResult.success(body); } } }interceptorJwtInterceptor/** * @author Johnny * @Date: 2020/4/23 19:41 * @Description: */ @Slf4j @Component public class JwtInterceptor implements HandlerInterceptor { @Resource SysRoleService roleService; @Resource private RedisService redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); log.info("token———————————"+token); //认证 if (token == null) { log.info("用户未登录"); return false; }else{ //登录之后 //从请求头当中取出用户id String adminId = request.getHeader("id"); log.info("用户id" + adminId); //在redis中检查是否存在adminId为key的数据 if (!redisService.existsKey(adminId)) { log.info("redis中不存在此键"); }else{ String secrect = redisService.getValue(adminId, String.class); //从token中解析出roles字符串 String roles = JwtTokenUtil.getRoles(token,secrect); //反序列化成List List<SysRole> sysRoles = JSONArray.parseArray(roles, SysRole.class); //从request中取得客户端传输的roleId String roleId = request.getParameter("roleId"); log.info("roleId:"+roleId); boolean flag = roleService.checkRole(sysRoles,Integer.parseInt(roleId)); if(flag){ return true; }else{ log.info("用户不具备此角色"); } } return false; } } }LoginInterceptor/** * @author Johnny * @Date: 2020/4/21 21:00 * @Description: */ @Slf4j @Component public class LoginInterceptor implements HandlerInterceptor { @Resource private RedisService redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handle) { //将request包装成HttpServletWrapper类型 RequestWrapper requestWrapper = new RequestWrapper(request); //取得请求的json对象 String body = requestWrapper.getBody(); log.info(body); //从redis中取得指定用户名的验证码 JSONObject jsonObject = JSONObject.parseObject(body); String name = jsonObject.getString("name"); System.out.println(name); String password = jsonObject.getString("password"); System.out.println(password); String verifyCode = jsonObject.getString("verifyCode"); System.out.println(verifyCode); LoginDto loginDto = LoginDto.builder().name(name).password(password).verifyCode(verifyCode).build(); return true; // if (redisService.existsKey(loginDto.getName())) { // //获取redis中的验证码 // String correctCode = redisService.getValue(name, String.class); // //忽略大小写，成功则放行到controller调用登录接口 // if (correctCode.equalsIgnoreCase(verifyCode)) { // return true; // }else{ // log.info("验证码错误"); // return false; // } // }else{ // log.info("验证码失效"); // return false; // } } }mapper层这个目前不做过多赘述，我会尽快整理下mybatisplus的常用语法发出来util层ExcelConsumer/** * @author Johnny * @Date: 2020/4/28 21:53 * @Description: */ @Slf4j public class ExcelConsumer<T> implements Runnable { /** * 一张工作表可以容纳的最大行数 */ private static Integer SHEET_SIZE = 100000; /** * 导出的Vo数据类型 */ private Class<T> clazz; /** * 工作簿 */ private SXSSFWorkbook wb; /** * 工作表名称 */ private String sheetName; /** * 数据缓冲区对象 */ private ExportDataAdapter<T> exportDataAdapter; /** * 线程同步 */ private CountDownLatch latch; /** * 构造方法 * * @param clazz */ public ExcelConsumer(Class<T> clazz, ExportDataAdapter<T> exportDataAdapter, SXSSFWorkbook wb, CountDownLatch latch, String sheetName) { if (clazz == null || wb == null || exportDataAdapter == null || latch == null) { log.error("ExcelConsumer::初始化对象参数不能为空"); return; } this.clazz = clazz; this.exportDataAdapter = exportDataAdapter; this.wb = wb; this.latch = latch; this.sheetName = sheetName == null ? "UnNamedSheet" : sheetName; } @Override public void run() { //初始化excel导出工具类 ExcelUtil<T> excelUtil = new ExcelUtil<>(this.clazz); Sheet sheet = null; int sheetNo = 0; int rowNum = 1; T vo; //生产者还在生产数据 while (latch.getCount() > 1) { //生成sheetName if (rowNum == 1) { sheetNo++; sheet = excelUtil.createSheet(wb, sheetName.concat(Integer.toString(sheetNo))); excelUtil.setColumnTitle(sheet); } //获取数据 vo = exportDataAdapter.getData(); //往excel添加一行数据 excelUtil.addRowData(vo, wb, sheet, rowNum); rowNum++; //准备生成下一个sheetName if (rowNum == SHEET_SIZE + 1) { rowNum = 1; } } //生产者不再生产数据，取剩余数据，并将数据写入excel Integer reminderDataSize = exportDataAdapter.getDataSize(); T reminderData; if (reminderDataSize > 0) { for (int i = 0; i < reminderDataSize; i++) { reminderData = exportDataAdapter.getData(); if (rowNum == 1) { sheetNo++; sheet = excelUtil.createSheet(wb, sheetName.concat(Integer.toString(sheetNo))); excelUtil.setColumnTitle(sheet); } excelUtil.addRowData(reminderData, wb, sheet, rowNum); rowNum++; if (rowNum == SHEET_SIZE + 1) { rowNum = 1; } } } log.info("数据导出完成"); latch.countDown(); } }ExcelUtil/** * @author Johnny * @Date: 2020/4/28 21:47 * @Description: */ public class ExcelUtil<T> { Class<T> clazz; /** * 表头字段列表 */ private List<Field> fields; /** * 数字单元格对象 */ private CellStyle decimalCellStyle = null; /** * 日期时间单元格对象 */ private CellStyle dateTimeCellStyle = null; /** * 构造方法 * * @param clazz */ public ExcelUtil(Class<T> clazz) { this.clazz = clazz; } /** * 添加一条数据 * * @param vo:需要导出的vo对象 * @param wb：工作簿 * @param sheet：工作表 * @param rowNum：当前行号 */ public void addRowData(T vo, SXSSFWorkbook wb, Sheet sheet, int rowNum) { //创建一行 Row row = sheet.createRow(rowNum); Field field; Cell cell; ExcelVoAttribute attr; int fieldSize = fields.size(); // 遍历入参对象的所有属性 for (int j = 0; j < fieldSize; j++) { // 通过反射获得需要导出的入参对象的所有属性 field = fields.get(j); // 设置实体类私有属性可访问 field.setAccessible(true); // 获取所有添加了注解的属性 attr = field.getAnnotation(ExcelVoAttribute.class); // 给每个属性创建一个单元格 cell = row.createCell(j); try { this.setCellValue(attr, field.get(vo), wb, cell); } catch (IllegalAccessException e) { e.printStackTrace(); } } } /** * 根据注解类判断字段类型设置excel单元格数据格式方法 * * @param attr * @param valueObject * @param workbook * @param cell */ private void setCellValue(ExcelVoAttribute attr, Object valueObject, SXSSFWorkbook workbook, Cell cell) { String returnValue; if (attr.isNumber()) { cell.setCellStyle(getCellStyle(attr, workbook)); returnValue = valueObject == null || "".equals(valueObject) ? "0" : valueObject.toString(); BigDecimal num = new BigDecimal(returnValue); cell.setCellValue(num.doubleValue()); } else if (attr.isDateTime()) { cell.setCellStyle(getCellStyle(attr, workbook)); DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); returnValue = df.format((TemporalAccessor) valueObject); cell.setCellValue(returnValue); } else { returnValue = valueObject == null ? "" : valueObject.toString(); cell.setCellValue(returnValue); } } /** * 根据注解类判断字段类型，返回excel单元格数据格式方法 * * @param attr * @param workbook * @return CellStyle */ private CellStyle getCellStyle(ExcelVoAttribute attr, SXSSFWorkbook workbook) { if (attr.isNumber()) { if (decimalCellStyle == null) { decimalCellStyle = workbook.createCellStyle(); //此处设置数字单元格格式 DataFormat df = workbook.createDataFormat(); //千分位,保留1位小数 decimalCellStyle.setDataFormat(df.getFormat("#,##0.0")); } return decimalCellStyle; } if (attr.isDateTime()) { if (dateTimeCellStyle == null) { dateTimeCellStyle = workbook.createCellStyle(); //此处设置日期时间单元格格式 DataFormat df = workbook.createDataFormat(); dateTimeCellStyle.setDataFormat(df.getFormat("yyyy-MM-dd HH:mm:ss")); } return dateTimeCellStyle; } return null; } /** * 创建工作页Sheet * * @param wb * @param sheetName * @return Sheet */ public Sheet createSheet(SXSSFWorkbook wb, String sheetName) { return wb.createSheet(sheetName); } /** * 设置excel列头及格式 * * @param sheet */ public void setColumnTitle(Sheet sheet) { if (fields == null) { this.fields = this.getSortFields(); } Row row; Cell cell; ExcelVoAttribute attr; Field field; int fieldSize = fields.size(); row = sheet.createRow(0); for (int i = 0; i < fieldSize; i++) { field = fields.get(i); attr = field.getAnnotation(ExcelVoAttribute.class); cell = CellUtil.createCell(row, i, attr.name()); // 设置列宽，根据相应的字段名的长度等比 sheet.setColumnWidth(i, attr.name().getBytes().length * 400); } } /** * 获取输出对象字段列表，并根据注解进行字段排序 * * @return */ private List<Field> getSortFields() { List<Field> fields = Arrays.stream(clazz.getDeclaredFields()).filter(x -> x.isAnnotationPresent(ExcelVoAttribute.class)).collect(Collectors.toList()); List<Field> sortList = new ArrayList<>(fields); //排序 for (Field field : fields) { ExcelVoAttribute excelVoAttribute = field.getAnnotation(ExcelVoAttribute.class); int sortNo = excelVoAttribute.column(); sortList.set(sortNo, field); } return sortList; } }ExcelUtils/** * @author Johnny * @Date: 2020/4/25 22:56 * @Description: */ @Slf4j public class ExcelUtils { /** * 导出歌曲 * @param response * @param list * @param map * @param title */ public static void exportExcel(HttpServletResponse response, List list, Map<String, String> map, String title) { //通过工具类创建writer ExcelWriter writer = ExcelUtil.getWriter(true); //自定义标题别名 Set<Map.Entry<String, String>> entries = map.entrySet(); //迭代器遍历数据 Iterator<Map.Entry<String, String>> iterator = entries.iterator(); while (iterator.hasNext()) { Map.Entry<String, String> next = iterator.next(); //自定义表头 writer.addHeaderAlias(next.getKey(), next.getValue()); } //合并单元格后的标题行，使用默认标题样式 writer.merge(map.size() - 1, title); //一次性写出内容，使用默认样式，强制输出标题 writer.write(list, true); //out为outputStream，需要写出到的目标流 try { writer.flush(response.getOutputStream(), true); } catch (IOException e) { log.info("歌单导出异常"); e.printStackTrace(); } writer.close(); } /** * 导入歌曲 * @param file * @return */ public static List<Song> importExcel(File file) { List<Song> songs = new ArrayList<>(); InputStream inputStream = null; try { inputStream = new FileInputStream(file); } catch (FileNotFoundException e) { e.printStackTrace(); } //通过getReader方法确定读取那个sheet中的数据 ExcelReader reader = ExcelUtil.getReader(inputStream, "sheet1"); //回去总行数 List<List<Object>> read = reader.read(1, reader.getRowCount()); for (List<Object> objects : read) { //对每行数据取出构建一个song对象 Song song = Song.builder() .songName(objects.get(0).toString()) .songId(UUID.randomUUID().toString().replace("-", "")) .sortId("0") .singer(objects.get(1).toString()) .duration(objects.get(2).toString()) .thumbnail(objects.get(3).toString()) .url(objects.get(4).toString()) .lyric(objects.get(5).toString()) .commentCount(0) .playCount(0) .deleteFlag("0") .updateTime(LocalDateTime.now()) .createTime(LocalDateTime.now()) .build(); songs.add(song); } return songs; } }ExportDataAdapter/** * @author Johnny * @Date: 2020/4/28 21:53 * @Description: */ public class ExportDataAdapter<T>{ /** * 默认队列大小 */ private static Integer DEFAULT_SIZE = 1000; private BlockingQueue<T> resourceQueue = null; public ExportDataAdapter() { this.resourceQueue = new LinkedBlockingQueue<T>(DEFAULT_SIZE); } /** * 添加数据 * * @param data */ public void addData(T data) { try { resourceQueue.put(data); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 获取剩余数据数量 * * @return */ public Integer getDataSize() { return resourceQueue.size(); } /** * 从队列中获取数据 * * @return */ public T getData() { try { return resourceQueue.take(); } catch (InterruptedException e) { e.printStackTrace(); } return null; } }FileUtil/** * @author Johnny * @Date: 2020/4/26 10:41 * @Description: */ public class FileUtil { public static File fileConversion(MultipartFile file) { int n; File file1 = new File(file.getOriginalFilename()); try { InputStream in = file.getInputStream(); OutputStream os = new FileOutputStream(file1); byte[] bytes = new byte[4096]; while ((n=in.read(bytes,0,4096))!=-1){ os.write(bytes,0,n); File file2 = new File(file1.toURI()); file2.delete(); } } catch (IOException e) { e.printStackTrace(); } return file1; } }JwtTokenUtil/** * @Author Johnny * @Date 2020/4/15 * @Version 1.0 */ @Slf4j public class JwtTokenUtil { /** * 加密 * * @param userId * @param expiresAt * @return String */ public static String getToken(final String userId, String userRole,final String secrect,Date expiresAt) { String token = null; try { token = JWT.create() .withIssuer("auth0") .withClaim("userId", userId) .withClaim("userRole", userRole) .withExpiresAt(expiresAt) // 使用了HMAC256加密算法, mySecret是用来加密数字签名的密钥 .sign(Algorithm.HMAC256(secrect)); } catch (UnsupportedEncodingException e) { log.error("不支持的编码格式"); } return token; } /** * 解密 * @param token * @return DecodedJWT */ public static DecodedJWT deToken(final String token,final String secrect) { DecodedJWT jwt; JWTVerifier verifier = null; try { verifier = JWT.require(Algorithm.HMAC256(secrect)) .withIssuer("auth0") .build(); } catch (UnsupportedEncodingException e) { log.error("不支持的编码格式"); } assert verifier != null; jwt = verifier.verify(token); return jwt; } /** * 获取userId * * @param token * @return String */ public static String getUserId(String token,String secrect) { return deToken(token,secrect).getClaim("userId").asString(); } /** * 获取role * * @param token * @return String */ public static String getRoles(String token,String secrect) { return deToken(token,secrect).getClaim("userRole").asString(); } /** * 验证是否过期 * * @param token * @return boolean */ public static boolean isExpiration(String token,String secrect) { return deToken(token,secrect).getExpiresAt().before(new Date()); } public static void main(String[] args) { // String token = getToken("2000100193", new Date(System.currentTimeMillis() + 10L * 1000L)); // System.out.println(token); // while (true) { // boolean flag = isExpiration(token); // System.out.println(flag); // if (flag) { // System.out.println("token已失效"); // break; // } // try { // Thread.sleep(1000); // } catch (InterruptedException e) { // e.printStackTrace(); // } // } // String token = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJhdXRoMCIsImV4cCI6MTU4NzY3MTQ3OCwidXNlcklkIjoiREUzNUQ3Q0MwNUFGOTZBMjFEN0FERkM4NjUxRTY2MTQifQ.uT2W3QcE3744WNN3inEKT8lUVJs6xAC7TodDCaWkcyM"; // System.out.println(deToken(token).getClaim("userId").asString()); } }Md5Util/** * @author Johnny */ public class Md5Util { /** * @param pwd 需要加密的字符串 * @param isUpper 字母大小写(false为默认小写，true为大写) * @param bit 加密的位数（16,32,64） * @return String */ public static String getMd5(String pwd, boolean isUpper, Integer bit) { String md5 = ""; try { // 创建加密对象 MessageDigest md = MessageDigest.getInstance("md5"); if (bit == 64) { Base64.Encoder encoder = Base64.getEncoder(); md5 = encoder.encodeToString(md.digest(pwd.getBytes(StandardCharsets.UTF_8))); } else { // 计算MD5函数 md.update(pwd.getBytes()); byte b[] = md.digest(); int i; StringBuilder sb = new StringBuilder(); for (byte value : b) { i = value; if (i < 0) { i += 256; } if (i < 16) { sb.append("0"); } sb.append(Integer.toHexString(i)); } md5 = sb.toString(); if (bit == 16) { //截取32位md5为16位 md5 = md5.substring(8, 24).toString(); if (isUpper) { md5 = md5.toUpperCase(); } return md5; } } //转换成大写 if (isUpper) { md5 = md5.toUpperCase(); } } catch (Exception e) { e.printStackTrace(); System.out.println("md5加密抛出异常！"); } return md5; } public static void main(String[] args) { String a = "123456"; String md5a = getMd5(a, true, 32); System.out.println(md5a); System.out.println(md5a.length()); }ThreadPool/** * @author Johnny * @Date: 2020/4/28 21:55 * @Description: */ public class ThreadPool { /** * 异步线程 */ private final static ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 100, 10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(2000), r -> new Thread(r, "excelExportThread"), new ThreadPoolExecutor.AbortPolicy()); public static ThreadPoolExecutor getExecutor() { return executor; } }TreeBuilder/** * @author Johnny * @Date: 2020/4/23 18:46 * @Description: */ public class TreeBuilder { /** * 两层循环实现建树 * * @param treeNodes 传入的树节点列表 * @return */ public static List<TreeNode> buildTreeByLoop(List<TreeNode> treeNodes) { List<TreeNode> trees = new ArrayList<>(); for (TreeNode treeNode : treeNodes) { if (treeNode.getParentId() == 0) { trees.add(treeNode); } for (TreeNode it : treeNodes) { if (it.getParentId().equals(treeNode.getId())) { if (treeNode.getSubMenus() == null) { treeNode.setSubMenus(new ArrayList<>()); } treeNode.getSubMenus().add(it); } } } return trees; } /** * 使用递归方法建树 * * @param treeNodes * @return */ public static List<TreeNode> buildTreeByRecursive(List<TreeNode> treeNodes) { List<TreeNode> trees = new ArrayList<>(); for (TreeNode treeNode : treeNodes) { if (treeNode.getParentId() == 0) { trees.add(findChildren(treeNode, treeNodes)); } } return trees; } /** * 递归查找子节点 * * @param treeNodes * @return */ public static TreeNode findChildren(TreeNode treeNode, List<TreeNode> treeNodes) { for (TreeNode it : treeNodes) { if (treeNode.getId().equals(it.getParentId())) { if (treeNode.getSubMenus() == null) { treeNode.setSubMenus(new ArrayList<>()); } treeNode.getSubMenus().add(findChildren(it, treeNodes)); } } return treeNode; } }TreeNode/** * @author Johnny * @Date: 2020/4/23 18:46 * @Description: */ @Data @NoArgsConstructor @AllArgsConstructor @Builder public class TreeNode { private Integer id; private Integer parentId; private Integer type; private String title; private String icon; private String path; private Integer sort; private List<TreeNode> subMenus; public TreeNode(Integer id, Integer parentId, Integer type,String title, String icon, String path, Integer sort) { this.id = id; this.parentId = parentId; this.type = type; this.title = title; this.icon = icon; this.path = path; this.sort = sort; } }
什么是面向切面编程官方：在软件业，AOP为Aspect Oriented Programming的缩写，意为：面向切面编程，通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续，是软件开发中的一个热点，也是Spring框架中的一个重要内容，是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离，从而使得业务逻辑各部分之间的耦合度降低，提高程序的可重用性，同时提高了开发的效率个人理解：面向切面编程旨在较少冗余代码，使用的是一种代码横向抽取（举例：不同的方法拥有三句同样的代码，那这个代码就可以抽取出来，但使用父类、接口、继承、实现会极大的提高代码的耦合度，因此我们采用面向切面编程，把这三句代码全部抽取到一个方法当中）xml配置版实例代码接口：public interface Hello { void sayHello(); } 实现类public class HelloImpl implements Hello { @Override public void sayHello() { System.out.println("hello"); } }增强（相当于抽取出来的部分代码）public class BuildHello { void beforeBuilder(){ System.out.println("start..."); } void afterBuilder(){ System.out.println("ending..."); } } xml中bean实例的配置 <bean id="hello" class="com.soft1851.spring.aop.hello.HelloImpl"/> <bean id="beforeAdvice" class="com.soft1851.spring.aop.hello.BuildHello"/> <aop:config > <aop:aspect id="before" ref="beforeAdvice"> <aop:pointcut id="beforeCut" expression="execution(* com.soft1851.spring.aop.hello..*.*(..))"/> <aop:before method="beforeBuilder" pointcut-ref="beforeCut"/> </aop:aspect> <aop:aspect id="after" ref="beforeAdvice"> <aop:pointcut id="afterCut" expression="execution(* com.soft1851.spring.aop.hello..*.*(..))"/> <aop:after-returning method="afterBuilder" pointcut-ref="afterCut"/> </aop:aspect> </aop:config>注意要点：aop-config是配置整体的切面编程，而aop-aspect是设置切点和增强的容器，aop-pointcun用于设置切点，aop:before用于设置前置增强，aop:after-returning用于设置后置增强注解版实例代码dao底层代码@Repository public class ForumDaoImpl implements ForumDao { private JdbcTemplate jdbcTemplate; public ForumDaoImpl(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public List<Forum> selectAll() { String sql = "SELECT * FROM t_forum "; System.out.println(jdbcTemplate); return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Forum.class)); } @Override public int insert(Forum forum) { System.out.println("开始插入数据"); String sql = "INSERT INTO t_forum VALUES (Null,?)"; Object[] args = {forum.getForumName()}; return jdbcTemplate.update(sql, args); } @Override public int[] batchInsert(List<Forum> forums) { String sql = "INSERT INTO t_forum VALUES (NULL,?)"; return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement preparedStatement, int i) throws SQLException { preparedStatement.setString(1,forums.get(i).getForumName()); } @Override public int getBatchSize() { return forums.size(); } }); } @Override public int delete(int forumId) { System.out.println("开始删除数据"); String sql = "DELETE FROM t_forum WHERE forum_id = ?"; Object args = forumId; return jdbcTemplate.update(sql,args); } /** * 修改forum中的字段值 * @param forum * @return */ @Override public int update(Forum forum) { String sql = "UPDATE t_forum SET forum_name = ? WHERE forum_id = ?"; Object[] args = {forum.getForumName(), forum.getForumId()}; return jdbcTemplate.update(sql, args); } @Override public Forum get(int forumId) { String sql = " SELECT * FROM t_forum WHERE forum_id = ? "; Object[] args = {forumId}; return jdbcTemplate.queryForObject(sql, args,new BeanPropertyRowMapper<>(Forum.class)); } } 增强类@Aspect public class MyAdvice { /** * 配置切点 */ @Pointcut("execution(* com.soft1851.spring.web.dao..*.insert*(..))") public void pointCut() { } /** * 配置前置增强 */ @Before("MyAdvice.pointCut()") public void beforeMethod() { System.out.println("等待数据插入"); } /** * 配置后置增强 */ @AfterReturning("execution(* com.soft1851.spring.web.dao..*.*(..))") public void afterMethod(){ System.out.println("关闭数据库"); } } xml实例配置 <bean name="forumDao" class="com.soft1851.spring.web.dao.impl.ForumDaoImpl"/> <bean name="myAdvice" class="com.soft1851.spring.web.interceptor.MyAdvice"/> <aop:aspectj-autoproxy/>注意要点：增强类头部要写注解@Aspectxml文件中照样还需要配置，只是配置代码减少@afterReturning是后置增强（也就是中央代码执行结束后执行）aop支持的切点execution——匹配运行方法的连接点within——使匹配连接点限定在特定类型this——限定匹配的连接点是给定类型的实例target——限定匹配连接点的目标是给定类型的实例args——限定连接点的参数是给定类型的实例@target——限定匹配的连接点的运行的对象有该类型的注解@args——运行时传递的参数拥有给定类型的注解@within——限定匹配给定指定注解类型的连接点@annotation——限定连接点拥有指定的注解bean——使得连接点匹配特质的bean或bean集合exexction表达式匹配所有的公有方法execution(public (..))匹配所有以set开头的方法execution( set(..))匹配所有AccountService接口定义的方法execution( com.xyz.service.AccountService.(..))匹配所有service包内的方法execution(* com.xyz.service..(..))匹配所有service包以及子包内的方法execution(* com.xyz.service...(..))所有在service包内的连接点within(com.xyz.service.*)所有service包以及子包内的连接点within(com.xyz.service..*)所有实现AccountService接口的连接点this(com.xyz.service.AccountService)所有的目标对象实现AccountService接口的连接点target(com.xyz.service.AccountService)有一个参数并且运行时传入参数为Serializable的连接点args(java.io.Serializable)目标对象有@Transactional注解@target(org.springframework.transaction.annotation.Transactional)定义的目标对象类型含有@Transactional注解@within(org.springframework.transaction.annotation.Transactional)含有@Transactional注解的可运行方法@annotation(org.springframework.transaction.annotation.Transactional)含有一个参数并且在运行时传入的参数含有@Classified注解@args(com.xyz.security.Classified)bean名字定义为tradeService的连接点bean(tradeService)bean名字满足Service命名的所有连接点bean(Service)总结本文我们主要学习了如何使用XML配置方式来完成Spring AOP的使用。文章中我们学习了很多配置标签，完成了切面定义、切点定义以及多种增强方法的定义。至此我们已经掌握了Spring AOP的基本知识点，可在具体的项目开发中使用。不过学了那么多的知识点，我们还是需要一定的实践来进行消化吸收。