java開源項(xiàng)目jeecgboot的超詳細(xì)解析
一.搭建
1.前端
npm install npm run serve
2.后端
老生常談的配置,修改mysql與redis即可。
二.業(yè)務(wù)功能介紹
功能上jeecgboot主要提供了系列的代碼生成器、模板頁(yè)面、報(bào)表頁(yè)面。
1.報(bào)表功能
主要提供報(bào)表的相關(guān)操作。提供了積木報(bào)表插件,可以自定義數(shù)據(jù)報(bào)表、圖形報(bào)表。并將報(bào)表掛載到菜單上。
2.在線開發(fā)
也就是代碼生成器,可以可視化的在頁(yè)面上新建數(shù)據(jù)庫(kù)表,并通過(guò)數(shù)據(jù)庫(kù)表生成前后臺(tái)代碼。減少業(yè)務(wù)代碼開發(fā)的時(shí)間。
3.系統(tǒng)管理
用戶管理、角色管理、機(jī)構(gòu)管理、消息管理等基礎(chǔ)模塊。
4.系統(tǒng)監(jiān)控
主要負(fù)責(zé)各種日志、監(jiān)控的統(tǒng)一處理。
5.頁(yè)面組件樣式
常見案例、詳情頁(yè)、結(jié)果頁(yè)、異常頁(yè)、列表頁(yè)、表單頁(yè)主要提供了樣式頁(yè)面與控件頁(yè)面示例。在開發(fā)過(guò)程中如果需要模板直接復(fù)制代碼即可。詳情請(qǐng)
三.后臺(tái)架構(gòu)介紹
1.概括
其中報(bào)表和代碼生成器沒(méi)有提供源碼,如果有興趣可以自行查看jar包源碼。
2.架構(gòu)核心包jeecg-boot-base
jeecg-boot-base包括了下文的幾個(gè)部分。
1.接口包jeecg-boot-base-api
1.對(duì)外接口jeecg-system-cloud-api
使用feign+hystrix實(shí)現(xiàn)了服務(wù)間調(diào)用加熔斷,單機(jī)環(huán)境并沒(méi)有使用。
2.服務(wù)內(nèi)接口jeecg-system-local-api
該包提供了下文使用的常用方法接口。僅提供了接口并無(wú)其他配置。
2.核心配置包jeecg-boot-base-core
1.通用類common 1.api
其中為通用接口與通用返回對(duì)象。
1.Result
其中Result為所有類的返回實(shí)體,這樣能夠通過(guò)code編碼和message獲取是否成功和成功/失敗的信息。此類是常用的架構(gòu)設(shè)計(jì)
2.aspect
為項(xiàng)目的自定義注解,使用了AOP的切面方式實(shí)現(xiàn),這里就不詳細(xì)說(shuō)了,比較簡(jiǎn)單都可以看懂。
3.constant
存放著枚舉類與常量池,這里不多說(shuō)了。
4.es
為操作es的通用類,主要是配置es連接和查詢時(shí)動(dòng)態(tài)拼接and/or的方法。
5.exception
exception為自定義的異常類。
1.JeecgBootExceptionHandler
這里詳細(xì)說(shuō)一下JeecgBootExceptionHandler,該類也是常見的架構(gòu)設(shè)計(jì)之一,核心為@RestControllerAdvice、@ExceptionHandler
。當(dāng)業(yè)務(wù)代碼中沒(méi)有對(duì)異常攔截時(shí),該類會(huì)自動(dòng)攔截異常,并數(shù)據(jù)log日志。所以某些日志在該類配置后,就不需要在每個(gè)接口中都捕獲這個(gè)異常了。
6.handler
為下文規(guī)范提供了接口類。沒(méi)有其他特別說(shuō)明。
7.system類
這里主要說(shuō)controller、entity、service等業(yè)務(wù)代碼的父類
1.JeecgController<T, S extends IService>
所以controller的父類,提供了導(dǎo)入導(dǎo)出的功能。還可以在里面擴(kuò)展分頁(yè)、排序、常用調(diào)用方法等,這樣就可以避免相同的代碼多次添加。這也是架構(gòu)設(shè)計(jì)中常用的技巧。
2.JeecgEntity
將通用字段如id、創(chuàng)建人、修改人、創(chuàng)建時(shí)間、修改時(shí)間等字段統(tǒng)一封裝在一個(gè)實(shí)體中,使用其他實(shí)體繼承。這也是架構(gòu)設(shè)計(jì)中常用的技巧。
3.service
主要提供Mybatis-plus提供的curd方法。
8.utli
提供了一大波的工具類,如果在工作中需要,直接復(fù)制使用。
2.通用配置類config
1.mybatis
1.MybatisInterceptor
MybatisInterceptor這里主要說(shuō)MybatisInterceptor,該類負(fù)責(zé)在mybatis執(zhí)行語(yǔ)句前,攔截并獲取參數(shù),將創(chuàng)建人、創(chuàng)建時(shí)間等字符動(dòng)態(tài)插入。這里上部分核心代碼。
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; String sqlId = mappedStatement.getId(); log.debug("------sqlId------" + sqlId); //獲取sql類型是插入還是修改 SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); //獲取插入?yún)?shù) Object parameter = invocation.getArgs()[1]; if (parameter == null) { return invocation.proceed(); } if (SqlCommandType.INSERT == sqlCommandType) { LoginUser sysUser = this.getLoginUser(); //通過(guò)反射獲取入?yún)⒌念? Field[] fields = oConvertUtils.getAllFields(parameter); for (Field field : fields) { log.debug("------field.name------" + field.getName()); try { //將創(chuàng)建人信息動(dòng)態(tài)加入 if ("createBy".equals(field.getName())) { field.setAccessible(true); Object local_createBy = field.get(parameter); field.setAccessible(false); if (local_createBy == null || local_createBy.equals("")) { if (sysUser != null) { // 登錄人賬號(hào) field.setAccessible(true); field.set(parameter, sysUser.getUsername()); field.setAccessible(false); } } } }
2.MybatisPlusSaasConfig
該類主要負(fù)責(zé)多租戶,什么是多租戶呢?
多租戶:就是多個(gè)公司/客戶公用一套系統(tǒng)/數(shù)據(jù)庫(kù),這就需要保證數(shù)據(jù)的權(quán)限。
該場(chǎng)景比較少不詳細(xì)說(shuō)明。
2.oss
主要從application-dev.yml獲取到上傳的路徑與配置。
3.shiro
安全框架主要有兩個(gè)目標(biāo):認(rèn)證與鑒權(quán)。
認(rèn)證:判斷用戶名密碼是否正確。
鑒權(quán):判斷用戶是否有權(quán)限訪問(wèn)該接口。
這里本文著重講解,如果遇到shiro相關(guān)應(yīng)用,可以項(xiàng)目直接移植使用。
1.CustomShiroFilterFactoryBean
該類主要負(fù)責(zé)解決資源中文路徑問(wèn)題。這里有個(gè)通用的解決方式。
新建類集成ShiroFilterFactoryBean
方法,并重寫核心方法createInstance()
,并在注入時(shí),注入新建的類CustomShiroFilterFactoryBean
,這樣就達(dá)到的以往重新源碼的功能。因?yàn)閟pring提供的功能都是用該思想,所以修改源碼的地方就原來(lái)越少了,都可以使用該方式實(shí)現(xiàn)。
2.JwtFilter
同上文,復(fù)寫BasicHttpAuthenticationFilter
的驗(yàn)證登錄用戶的方法,在執(zhí)行登錄接口后判斷用戶是否正確。
3.ResourceCheckFilter
負(fù)責(zé)鑒權(quán)使用,判斷當(dāng)前用戶是否有權(quán)限訪問(wèn)。
//表示是否允許訪問(wèn) ,如果允許訪問(wèn)返回true,否則false; @Override protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception { Subject subject = getSubject(servletRequest, servletResponse); //獲取當(dāng)前url String url = getPathWithinApplication(servletRequest); log.info("當(dāng)前用戶正在訪問(wèn)的 url => " + url); return subject.isPermitted(url); } //onAccessDenied:表示當(dāng)訪問(wèn)拒絕時(shí)是否已經(jīng)處理了; 如果返回 true 表示需要繼續(xù)處理; 如果返回 false @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; response.sendRedirect(request.getContextPath() + this.errorUrl); // 返回 false 表示已經(jīng)處理,例如頁(yè)面跳轉(zhuǎn)啥的,表示不在走以下的攔截器了(如果還有配置的話) return false; }
4.ShiroRealm
主要負(fù)責(zé)獲取用戶所有的菜單權(quán)限,并提供token的一系列方法。
//獲取所有菜單權(quán)限集合 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { } //驗(yàn)證用戶輸入的賬號(hào)和密碼是否正確 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { } //校驗(yàn)token的有效性 public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException { } //刷新token有效時(shí)間 public boolean jwtTokenRefresh(String token, String userName, String passWord) { } //清除當(dāng)前用戶的權(quán)限認(rèn)證緩存 @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); }
5.ShiroConfig
此為shiro的核心配置類,大多數(shù)寫法都是固定寫法。
public class ShiroConfig { @Value("${jeecg.shiro.excludeUrls}") private String excludeUrls; @Resource LettuceConnectionFactory lettuceConnectionFactory; @Autowired private Environment env; /** * Filter Chain定義說(shuō)明 * * 1、一個(gè)URL可以配置多個(gè)Filter,使用逗號(hào)分隔 * 2、當(dāng)設(shè)置多個(gè)過(guò)濾器時(shí),全部驗(yàn)證通過(guò),才視為通過(guò) * 3、部分過(guò)濾器可指定參數(shù),如perms,roles */ @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 攔截器 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); if(oConvertUtils.isNotEmpty(excludeUrls)){ String[] permissionUrl = excludeUrls.split(","); for(String url : permissionUrl){ filterChainDefinitionMap.put(url,"anon"); } } // 配置不會(huì)被攔截的鏈接 順序判斷 也就是不同通過(guò)token訪問(wèn)的地址 filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); / // 添加自己的過(guò)濾器并且取名為jwt Map<String, Filter> filterMap = new HashMap<String, Filter>(1); //如果cloudServer為空 則說(shuō)明是單體 需要加載跨域配置【微服務(wù)跨域切換】 Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY); //前文定義的過(guò)濾器 filterMap.put("jwt", new JwtFilter(cloudServer==null)); shiroFilterFactoryBean.setFilters(filterMap); // <!-- 過(guò)濾鏈定義,從上向下順序執(zhí)行,一般將/**放在最為下邊 filterChainDefinitionMap.put("/**", "jwt"); // 未授權(quán)界面返回JSON shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } //加入security自定義配置類 @Bean("securityManager") public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //加入上文提供的授權(quán)配置 securityManager.setRealm(myRealm); DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); //使用redis緩存有關(guān)信息(實(shí)現(xiàn)在下文) securityManager.setCacheManager(redisCacheManager()); return securityManager; } //下面的代碼是添加注解支持 @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); defaultAdvisorAutoProxyCreator.setUsePrefix(true); defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor"); return defaultAdvisorAutoProxyCreator; } //管理shiro生命周期 @Bean public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } //開啟shiro注解 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } // 使用redis緩存用戶信息 ,并設(shè)置redis public RedisCacheManager redisCacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); //配置redis實(shí)例 redisCacheManager.setRedisManager(redisManager()); //redis中針對(duì)不同用戶緩存(此處的id需要對(duì)應(yīng)user實(shí)體中的id字段,用于唯一標(biāo)識(shí)) redisCacheManager.setPrincipalIdFieldName("id"); //用戶權(quán)限信息緩存時(shí)間 redisCacheManager.setExpire(200000); return redisCacheManager; } //連接redsi,分為單機(jī)與集群 @Bean public IRedisManager redisManager() { log.info("===============(2)創(chuàng)建RedisManager,連接Redis.."); IRedisManager manager; // redis 單機(jī)支持,在集群為空,或者集群無(wú)機(jī)器時(shí)候使用 add by jzyadmin@163.com if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) { RedisManager redisManager = new RedisManager(); redisManager.setHost(lettuceConnectionFactory.getHostName()); redisManager.setPort(lettuceConnectionFactory.getPort()); redisManager.setDatabase(lettuceConnectionFactory.getDatabase()); redisManager.setTimeout(0); if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) { redisManager.setPassword(lettuceConnectionFactory.getPassword()); } manager = redisManager; }else{ // redis集群支持,優(yōu)先使用集群配置 RedisClusterManager redisManager = new RedisClusterManager(); Set<HostAndPort> portSet = new HashSet<>(); lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort()))); //update-begin--Author:scott Date:20210531 for:修改集群模式下未設(shè)置redis密碼的bug issues/I3QNIC if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) { JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5, lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig()); redisManager.setPassword(lettuceConnectionFactory.getPassword()); redisManager.setJedisCluster(jedisCluster); } else { JedisCluster jedisCluster = new JedisCluster(portSet); redisManager.setJedisCluster(jedisCluster); } manager = redisManager; } return manager; } }
4.sign
這里不詳細(xì)講解,主要描述前臺(tái)傳來(lái)的簽名是否合法。
5.thirdapp
根據(jù)application-dev.yml配置獲取是否開啟第三方接入驗(yàn)證。@ConfigurationProperties(prefix = "third-app.type")
獲取配置文件中的third-app.type的value值。
6.AutoPoiConfig、AutoPoiDictConfig
主要負(fù)責(zé)將excel中的數(shù)據(jù)轉(zhuǎn)換為數(shù)據(jù)字典。
7.CorsFilterCondition、JeecgCloudCondition
通過(guò)獲取application-dev.yml,主要判斷是否有CLOUD_SERVER_KEY = "spring.cloud.nacos.discovery.server-addr";
的key值。
context.getEnvironment().getProperty(CommonConstant.CLOUD_SERVER_KEY);
是從application-dev.yml生成的map獲取value值。
8.RestTemplateConfig
在服務(wù)間調(diào)用時(shí)設(shè)置連接時(shí)長(zhǎng),如果單體應(yīng)用,改配置沒(méi)有使用。
9.StaticConfig
從application-dev.yml獲取配置,設(shè)置靜態(tài)參數(shù)初始化。
10.Swagger2Config
Swagger2文檔配置類,如果有需要請(qǐng)執(zhí)行復(fù)制使用。
11.WebMvcConfiguration
springboot的常用配置,如跨域配置,精度丟失配置,靜態(tài)資源配置。都是固定寫法,如果需要請(qǐng)自行參考。
12.WebSocketConfig
springboot提供的websocket的start配置方式,如果有疑問(wèn)可以參考博主之前的博文-websocket的集成使用
3.業(yè)務(wù)接口modules.base
主要提供了日志相關(guān)的curd,不多做描述。
3.工具包jeecg-boot-base-tools
主要提供了一些功能的實(shí)現(xiàn)類與使用方法,不多說(shuō) ,比較簡(jiǎn)單。
1.TransmitUserTokenFilter、UserTokenContext
主要負(fù)責(zé)將token放在上下文中。
2.JeecgRedisListerer、RedisReceiver
這里是發(fā)送消息模板的封裝。核心是從上下文中的getbean方法動(dòng)態(tài)的指定想要調(diào)用的JeecgRedisListerer實(shí)現(xiàn)類。一種多態(tài)的實(shí)現(xiàn)方式。
3.JeecgRedisCacheWriter
可以看到思想還是上文所說(shuō),將RedisCacheWriter類中的方法全部復(fù)制出來(lái),并生成新類JeecgRedisCacheWriter,在新類中修改,他的目的是信息模塊在存入緩存時(shí),有統(tǒng)一的前綴。在使用時(shí),注入使用JeecgRedisCacheWriter即可,跟修改源碼有這一樣效果,但是更加優(yōu)雅??梢钥闯鰏pring的設(shè)計(jì)思想是多牛批。
3.測(cè)試包jeecg-boot-base
主要負(fù)責(zé)調(diào)用其他功能,沒(méi)啥實(shí)質(zhì)意義。
下圖類是xxljob執(zhí)行定時(shí)任務(wù)時(shí)的寫法,可以看一看。
4.業(yè)務(wù)包jeecg-boot-module-system
主要為業(yè)務(wù)代碼包,這里找?guī)讉€(gè)著重講解一下。
1.api.controller
為微服務(wù)為其他服務(wù)提供基礎(chǔ)應(yīng)用接口的包,如果沒(méi)有微服務(wù)該包不生效。
2.message
該模塊為消息模塊業(yè)務(wù),其中使用了quartz和spring提供的websocket start。如果有興趣可以參考博主給的連接。
3.monitor
提供了redis監(jiān)控的接口。如果需要可以自行查看,比較簡(jiǎn)單。
4.quartz
定時(shí)任務(wù)start,如果想具體了解可以參考博主文章:quartz集成全解析
5.jeecg-boot-starter
springboot的核心就是提供各種各樣的start,在jeecg中也提供了很多的start,分別是上述的四個(gè),其中job為xxl開源項(xiàng)目、cloud為在服務(wù)間調(diào)用時(shí),將token放再頭中,這里不詳細(xì)講解。
下文示例均在jeecg-cloud-module/jeecg-cloud-system-start中。
1.jeecg-boot-starter-rabbitmq
1.MqListener、BaseRabbiMqHandler
在監(jiān)聽消息隊(duì)列時(shí),使用以下方法即可。原因是必須加入ack與nack。防止隊(duì)列占用。
使用時(shí),該類繼承了BaseRabbiMqHandler,并使用父類的方法,并使用new MqListener<BaseMap>()
函數(shù)式方法獲取消息隊(duì)列中的信息。
@Slf4j @RabbitListener(queues = CloudConstant.MQ_JEECG_PLACE_ORDER) @RabbitComponent(value = "helloReceiver1") public class HelloReceiver1 extends BaseRabbiMqHandler<BaseMap> { @RabbitHandler public void onMessage(BaseMap baseMap, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { //使用了父類的方法 super.onMessage(baseMap, deliveryTag, channel, new MqListener<BaseMap>() { @Override public void handler(BaseMap map, Channel channel) { //業(yè)務(wù)處理 String orderId = map.get("orderId").toString(); log.info("MQ Receiver1,orderId : " + orderId); } }); } }
BaseRabbiMqHandler主要的功能是提供了ack與nack,并將token放入頭中。
@Slf4j public class BaseRabbiMqHandler<T> { private String token= UserTokenContext.getToken(); public void onMessage(T t, Long deliveryTag, Channel channel, MqListener mqListener) { try { UserTokenContext.setToken(token); mqListener.handler(t, channel); channel.basicAck(deliveryTag, false); } catch (Exception e) { log.info("接收消息失敗,重新放回隊(duì)列"); try { /** * deliveryTag:該消息的index * multiple:是否批量.true:將一次性拒絕所有小于deliveryTag的消息。 * requeue:被拒絕的是否重新入隊(duì)列 */ channel.basicNack(deliveryTag, false, true); } catch (IOException ex) { ex.printStackTrace(); } } } } public interface MqListener<T> { default void handler(T map, Channel channel) { } }
2.RabbitMqClient
主要在隊(duì)列初始化時(shí)實(shí)現(xiàn)隊(duì)列的初始化,而是否初始化根據(jù)使用時(shí)的@RabbitListener、@RabbitComponent
判斷。
public interface MqListener<T> { default void handler(T map, Channel channel) { } } @Bean public void initQueue() { //獲取帶RabbitComponent注解的類 Map<String, Object> beansWithRqbbitComponentMap = this.applicationContext.getBeansWithAnnotation(RabbitComponent.class); Class<? extends Object> clazz = null; //循環(huán)map for (Map.Entry<String, Object> entry : beansWithRqbbitComponentMap.entrySet()) { log.info("初始化隊(duì)列............"); //獲取到實(shí)例對(duì)象的class信息 clazz = entry.getValue().getClass(); Method[] methods = clazz.getMethods(); //判斷是否有RabbitListener注解 RabbitListener rabbitListener = clazz.getAnnotation(RabbitListener.class); //類上有注解 就創(chuàng)建隊(duì)列 if (ObjectUtil.isNotEmpty(rabbitListener)) { createQueue(rabbitListener); } //方法上有注解 就創(chuàng)建隊(duì)列 for (Method method : methods) { RabbitListener methodRabbitListener = method.getAnnotation(RabbitListener.class); if (ObjectUtil.isNotEmpty(methodRabbitListener)) { createQueue(methodRabbitListener); } } } } /** * 初始化隊(duì)列 * * @param rabbitListener */ private void createQueue(RabbitListener rabbitListener) { String[] queues = rabbitListener.queues(); //創(chuàng)建交換機(jī) DirectExchange directExchange = createExchange(DelayExchangeBuilder.DELAY_EXCHANGE); rabbitAdmin.declareExchange(directExchange); //創(chuàng)建隊(duì)列 if (ObjectUtil.isNotEmpty(queues)) { for (String queueName : queues) { Properties result = rabbitAdmin.getQueueProperties(queueName); if (ObjectUtil.isEmpty(result)) { Queue queue = new Queue(queueName); addQueue(queue); Binding binding = BindingBuilder.bind(queue).to(directExchange).with(queueName); rabbitAdmin.declareBinding(binding); log.info("創(chuàng)建隊(duì)列:" + queueName); }else{ log.info("已有隊(duì)列:" + queueName); } } } }
3.RabbitMqConfig
為消息隊(duì)列的常用配置方式。這里不多描述。
4.event
這個(gè)包主要是為使用mq發(fā)送消息使用,多類別的消息會(huì)實(shí)現(xiàn)JeecgBusEventHandler
類,而BaseApplicationEvent
通過(guò)消息類型傳入的不同的參數(shù)選擇合適的業(yè)務(wù)類發(fā)送消息。
5.DelayExchangeBuilder
為延時(shí)隊(duì)列的交換機(jī)聲明與綁定。
2.jeecg-boot-starter-lock
1.如何使用分布式鎖
使用時(shí)有兩種方式,一種是使用注解方式,一種是使用redisson提供的API。
@Scheduled(cron = "0/5 * * * * ?") @JLock(lockKey = CloudConstant.REDISSON_DEMO_LOCK_KEY1) public void execute() throws InterruptedException { log.info("執(zhí)行execute任務(wù)開始,休眠三秒"); Thread.sleep(3000); System.out.println("=======================業(yè)務(wù)邏輯1============================="); Map map = new BaseMap(); map.put("orderId", "BJ0001"); rabbitMqClient.sendMessage(CloudConstant.MQ_JEECG_PLACE_ORDER, map); //延遲10秒發(fā)送 map.put("orderId", "NJ0002"); rabbitMqClient.sendMessage(CloudConstant.MQ_JEECG_PLACE_ORDER, map, 10000); log.info("execute任務(wù)結(jié)束,休眠三秒"); } public DemoLockTest() { } /** * 測(cè)試分布式鎖【編碼方式】 */ //@Scheduled(cron = "0/5 * * * * ?") public void execute2() throws InterruptedException { if (redissonLock.tryLock(CloudConstant.REDISSON_DEMO_LOCK_KEY2, -1, 6000)) { log.info("執(zhí)行任務(wù)execute2開始,休眠十秒"); Thread.sleep(10000); System.out.println("=======================業(yè)務(wù)邏輯2============================="); log.info("定時(shí)execute2結(jié)束,休眠十秒"); redissonLock.unlock(CloudConstant.REDISSON_DEMO_LOCK_KEY2); } else { log.info("execute2獲取鎖失敗"); } }
2.RepeatSubmitAspect
通過(guò)公平鎖判斷是否是多次點(diǎn)擊按鈕。
@Around("pointCut(jRepeat)") public Object repeatSubmit(ProceedingJoinPoint joinPoint,JRepeat jRepeat) throws Throwable { String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod()); if (Objects.nonNull(jRepeat)) { // 獲取參數(shù) Object[] args = joinPoint.getArgs(); // 進(jìn)行一些參數(shù)的處理,比如獲取訂單號(hào),操作人id等 StringBuffer lockKeyBuffer = new StringBuffer(); String key =getValueBySpEL(jRepeat.lockKey(), parameterNames, args,"RepeatSubmit").get(0); // 公平加鎖,lockTime后鎖自動(dòng)釋放 boolean isLocked = false; try { isLocked = redissonLockClient.fairLock(key, TimeUnit.SECONDS, jRepeat.lockTime()); // 如果成功獲取到鎖就繼續(xù)執(zhí)行 if (isLocked) { // 執(zhí)行進(jìn)程 return joinPoint.proceed(); } else { // 未獲取到鎖 throw new Exception("請(qǐng)勿重復(fù)提交"); } } finally { // 如果鎖還存在,在方法執(zhí)行完成后,釋放鎖 if (isLocked) { redissonLockClient.unlock(key); } } } return joinPoint.proceed(); }
3.DistributedLockHandler
該類主要是jLock的切面類,通過(guò)jLock注解參數(shù),判斷需要加鎖的類型,同時(shí)加鎖的方法也不相同。
//jLock切面,進(jìn)行加鎖 @SneakyThrows @Around("@annotation(jLock)") public Object around(ProceedingJoinPoint joinPoint, JLock jLock) { Object obj = null; log.info("進(jìn)入RedisLock環(huán)繞通知..."); RLock rLock = getLock(joinPoint, jLock); boolean res = false; //獲取超時(shí)時(shí)間 long expireSeconds = jLock.expireSeconds(); //等待多久,n秒內(nèi)獲取不到鎖,則直接返回 long waitTime = jLock.waitTime(); //執(zhí)行aop if (rLock != null) { try { if (waitTime == -1) { res = true; //一直等待加鎖 rLock.lock(expireSeconds, TimeUnit.MILLISECONDS); } else { res = rLock.tryLock(waitTime, expireSeconds, TimeUnit.MILLISECONDS); } if (res) { obj = joinPoint.proceed(); } else { log.error("獲取鎖異常"); } } finally { if (res) { rLock.unlock(); } } } log.info("結(jié)束RedisLock環(huán)繞通知..."); return obj; } //通過(guò)參數(shù)判斷加鎖類型 @SneakyThrows private RLock getLock(ProceedingJoinPoint joinPoint, JLock jLock) { //獲取key String[] keys = jLock.lockKey(); if (keys.length == 0) { throw new RuntimeException("keys不能為空"); } //獲取參數(shù) String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod()); Object[] args = joinPoint.getArgs(); LockModel lockModel = jLock.lockModel(); if (!lockModel.equals(LockModel.MULTIPLE) && !lockModel.equals(LockModel.REDLOCK) && keys.length > 1) { throw new RuntimeException("參數(shù)有多個(gè),鎖模式為->" + lockModel.name() + ".無(wú)法鎖定"); } RLock rLock = null; String keyConstant = jLock.keyConstant(); //判斷鎖類型 if (lockModel.equals(LockModel.AUTO)) { if (keys.length > 1) { lockModel = LockModel.REDLOCK; } else { lockModel = LockModel.REENTRANT; } } //根據(jù)不同的鎖類型執(zhí)行不同的加鎖方式 switch (lockModel) { case FAIR: rLock = redissonClient.getFairLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0)); break; case REDLOCK: List<RLock> rLocks = new ArrayList<>(); for (String key : keys) { List<String> valueBySpEL = getValueBySpEL(key, parameterNames, args, keyConstant); for (String s : valueBySpEL) { rLocks.add(redissonClient.getLock(s)); } } RLock[] locks = new RLock[rLocks.size()]; int index = 0; for (RLock r : rLocks) { locks[index++] = r; } rLock = new RedissonRedLock(locks); break; case MULTIPLE: rLocks = new ArrayList<>(); for (String key : keys) { List<String> valueBySpEL = getValueBySpEL(key, parameterNames, args, keyConstant); for (String s : valueBySpEL) { rLocks.add(redissonClient.getLock(s)); } } locks = new RLock[rLocks.size()]; index = 0; for (RLock r : rLocks) { locks[index++] = r; } rLock = new RedissonMultiLock(locks); break; case REENTRANT: List<String> valueBySpEL = getValueBySpEL(keys[0], parameterNames, args, keyConstant); //如果spel表達(dá)式是數(shù)組或者LIST 則使用紅鎖 if (valueBySpEL.size() == 1) { rLock = redissonClient.getLock(valueBySpEL.get(0)); } else { locks = new RLock[valueBySpEL.size()]; index = 0; for (String s : valueBySpEL) { locks[index++] = redissonClient.getLock(s); } rLock = new RedissonRedLock(locks); } break; case READ: rLock = redissonClient.getReadWriteLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0)).readLock(); break; case WRITE: rLock = redissonClient.getReadWriteLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0)).writeLock(); break; } return rLock; }
4.RedissonLockClient
redisson客戶端,提供了一大波方法,請(qǐng)自行查看。
public class RedissonLockClient { @Autowired private RedissonClient redissonClient; @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 獲取鎖 */ public RLock getLock(String lockKey) { return redissonClient.getLock(lockKey); } /** * 加鎖操作 * * @return boolean */ public boolean tryLock(String lockName, long expireSeconds) { return tryLock(lockName, 0, expireSeconds); } . . .
5.core包
主要通過(guò)application.yml配置文件獲取redis連接類型,通過(guò)根據(jù)該參數(shù)動(dòng)態(tài)的選擇策略類,連接redis。
public class RedissonManager { public Redisson getRedisson() { return redisson; } //Redisson連接方式配置工廠 static class RedissonConfigFactory { private RedissonConfigFactory() { } private static volatile RedissonConfigFactory factory = null; public static RedissonConfigFactory getInstance() { if (factory == null) { synchronized (Object.class) { if (factory == null) { factory = new RedissonConfigFactory(); } } } return factory; } //根據(jù)連接類型創(chuàng)建連接方式的配置 Config createConfig(RedissonProperties redissonProperties) { Preconditions.checkNotNull(redissonProperties); Preconditions.checkNotNull(redissonProperties.getAddress(), "redis地址未配置"); RedisConnectionType connectionType = redissonProperties.getType(); // 聲明連接方式 RedissonConfigStrategy redissonConfigStrategy; if (connectionType.equals(RedisConnectionType.SENTINEL)) { redissonConfigStrategy = new SentinelRedissonConfigStrategyImpl(); } else if (connectionType.equals(RedisConnectionType.CLUSTER)) { redissonConfigStrategy = new ClusterRedissonConfigStrategyImpl(); } else if (connectionType.equals(RedisConnectionType.MASTERSLAVE)) { redissonConfigStrategy = new MasterslaveRedissonConfigStrategyImpl(); } else { redissonConfigStrategy = new StandaloneRedissonConfigStrategyImpl(); } Preconditions.checkNotNull(redissonConfigStrategy, "連接方式創(chuàng)建異常"); return redissonConfigStrategy.createRedissonConfig(redissonProperties); } } } //策略實(shí)現(xiàn),此類是指定redis的連接方式是哨兵。 public class SentinelRedissonConfigStrategyImpl implements RedissonConfigStrategy { @Override public Config createRedissonConfig(RedissonProperties redissonProperties) { Config config = new Config(); try { String address = redissonProperties.getAddress(); String password = redissonProperties.getPassword(); int database = redissonProperties.getDatabase(); String[] addrTokens = address.split(","); String sentinelAliasName = addrTokens[0]; // 設(shè)置redis配置文件sentinel.conf配置的sentinel別名 config.useSentinelServers().setMasterName(sentinelAliasName); config.useSentinelServers().setDatabase(database); if (StringUtils.isNotBlank(password)) { config.useSentinelServers().setPassword(password); } // 設(shè)置哨兵節(jié)點(diǎn)的服務(wù)IP和端口 for (int i = 1; i < addrTokens.length; i++) { config.useSentinelServers().addSentinelAddress(GlobalConstant.REDIS_CONNECTION_PREFIX+ addrTokens[i]); } log.info("初始化哨兵方式Config,redisAddress:" + address); } catch (Exception e) { log.error("哨兵Redisson初始化錯(cuò)誤", e); e.printStackTrace(); } return config; } }
6.jeecg-cloud-module
這里詳細(xì)的說(shuō)一下jeecg-cloud-gateway,因?yàn)槠渌亩际情_源項(xiàng)目沒(méi)下載即用。
jeecg-cloud-system-start為封裝start的使用方法,上文已經(jīng)介紹了。
1.jeecg-cloud-gateway
1.GatewayRoutersConfiguration
當(dāng)固定的幾個(gè)路由,有特殊化的執(zhí)行方法。
2.RateLimiterConfiguration
主要配置限流,與application.yml一起使用,下文配置含義是,發(fā)送過(guò)來(lái)的請(qǐng)求只能容納redis-rate-limiter.burstCapacity
的配置(3次)多余的會(huì)全部丟棄(限流),每秒消費(fèi)redis-rate-limiter.replenishRate
(1次)。
3.FallbackController
熔斷的執(zhí)行方法。
4.GlobalAccessTokenFilter
全局?jǐn)r截器,在調(diào)用其他服務(wù)時(shí),將用戶信息放在請(qǐng)求頭中。
5.SentinelFilterContextConfig
使Sentinel鏈路流控模式生效,固定寫法。
6.HystrixFallbackHandler、SentinelBlockRequestHandler
在降級(jí)/限流時(shí),將異常信息轉(zhuǎn)換成json返回給前臺(tái)。
7.LoderRouderHandler
動(dòng)態(tài)刷新路由。
8.MySwaggerResourceProvider、SwaggerResourceController
將swagger地址統(tǒng)一管理起來(lái)
9.DynamicRouteLoader、DynamicRouteService
DynamicRouteLoader:通過(guò)application.yml判斷從nacos/redis中獲取路由信息,并實(shí)現(xiàn)動(dòng)態(tài)的加載。
DynamicRouteService:為底層處理路由的API。
四.總體感想
文章到這里差不多就接近尾聲了,大多數(shù)功能附帶著代碼都講述了一遍。在功能上來(lái)說(shuō),jeecg提供了很多常用功能,如rabbitMq封裝、積木報(bào)表、代碼生成器等。這些在日常工作中有很大的概率碰上,如果有以上需求,可以來(lái)框架中直接復(fù)制粘貼即可。
但是在格式規(guī)范上,如出入?yún)⒌囊?guī)范,代碼的寫法,代碼的格式化等方面,并不是特別統(tǒng)一,且沒(méi)有嚴(yán)格規(guī)范。總體來(lái)說(shuō)非常適合做私活與畢業(yè)設(shè)計(jì),同時(shí)也是最早一批開源的前后端項(xiàng)目腳手架,爆贊。
到此這篇關(guān)于java開源項(xiàng)目jeecgboot的文章就介紹到這了,更多相關(guān)java開源項(xiàng)目jeecgboot內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot ApplicationRunner的使用解讀
這篇文章主要介紹了Springboot ApplicationRunner的使用解讀,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05Java實(shí)現(xiàn)掃雷游戲詳細(xì)代碼講解
windows自帶的游戲《掃雷》是陪伴了無(wú)數(shù)人的經(jīng)典游戲,本文將利用Java語(yǔ)言實(shí)現(xiàn)這一經(jīng)典的游戲,文中的示例代碼講解詳細(xì),感興趣的可以學(xué)習(xí)一下2022-05-05微信小程序調(diào)用微信登陸獲取openid及java做為服務(wù)端示例
這篇文章主要介紹了微信小程序調(diào)用微信登陸獲取openid及java做為服務(wù)端示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01Java并發(fā)系列之CyclicBarrier源碼分析
這篇文章主要為大家詳細(xì)分析了Java并發(fā)系列之CyclicBarrier源碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03AsyncHttpClient KeepAliveStrategy源碼流程解讀
這篇文章主要為大家介紹了AsyncHttpClient KeepAliveStrategy源碼流程解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12Spring boot 連接多數(shù)據(jù)源過(guò)程詳解
這篇文章主要介紹了Spring boot 連接多數(shù)據(jù)源過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08關(guān)于eclipse中運(yùn)行tomcat提示端口被占用的4種解決
這篇文章主要介紹了關(guān)于eclipse中運(yùn)行tomcat提示端口被占用的4種解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01