java開(kāi)源項(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.在線(xiàn)開(kāi)發(fā)
也就是代碼生成器,可以可視化的在頁(yè)面上新建數(shù)據(jù)庫(kù)表,并通過(guò)數(shù)據(jù)庫(kù)表生成前后臺(tái)代碼。減少業(yè)務(wù)代碼開(kāi)發(fā)的時(shí)間。

3.系統(tǒng)管理
用戶(hù)管理、角色管理、機(jī)構(gòu)管理、消息管理等基礎(chǔ)模塊。

4.系統(tǒng)監(jiān)控
主要負(fù)責(zé)各種日志、監(jiān)控的統(tǒng)一處理。

5.頁(yè)面組件樣式
常見(jiàn)案例、詳情頁(yè)、結(jié)果頁(yè)、異常頁(yè)、列表頁(yè)、表單頁(yè)主要提供了樣式頁(yè)面與控件頁(yè)面示例。在開(kāi)發(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.通用類(lèi)common 1.api
其中為通用接口與通用返回對(duì)象。
1.Result
其中Result為所有類(lèi)的返回實(shí)體,這樣能夠通過(guò)code編碼和message獲取是否成功和成功/失敗的信息。此類(lèi)是常用的架構(gòu)設(shè)計(jì)

2.aspect
為項(xiàng)目的自定義注解,使用了AOP的切面方式實(shí)現(xiàn),這里就不詳細(xì)說(shuō)了,比較簡(jiǎn)單都可以看懂。

3.constant
存放著枚舉類(lèi)與常量池,這里不多說(shuō)了。


4.es
為操作es的通用類(lèi),主要是配置es連接和查詢(xún)時(shí)動(dòng)態(tài)拼接and/or的方法。

5.exception
exception為自定義的異常類(lèi)。
1.JeecgBootExceptionHandler
這里詳細(xì)說(shuō)一下JeecgBootExceptionHandler,該類(lèi)也是常見(jiàn)的架構(gòu)設(shè)計(jì)之一,核心為@RestControllerAdvice、@ExceptionHandler。當(dāng)業(yè)務(wù)代碼中沒(méi)有對(duì)異常攔截時(shí),該類(lèi)會(huì)自動(dòng)攔截異常,并數(shù)據(jù)log日志。所以某些日志在該類(lèi)配置后,就不需要在每個(gè)接口中都捕獲這個(gè)異常了。

6.handler
為下文規(guī)范提供了接口類(lèi)。沒(méi)有其他特別說(shuō)明。

7.system類(lèi)
這里主要說(shuō)controller、entity、service等業(yè)務(wù)代碼的父類(lèi)

1.JeecgController<T, S extends IService>
所以controller的父類(lèi),提供了導(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
提供了一大波的工具類(lèi),如果在工作中需要,直接復(fù)制使用。

2.通用配置類(lèi)config

1.mybatis

1.MybatisInterceptor
MybatisInterceptor這里主要說(shuō)MybatisInterceptor,該類(lèi)負(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類(lèi)型是插入還是修改
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)⒌念?lèi)
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
該類(lèi)主要負(fù)責(zé)多租戶(hù),什么是多租戶(hù)呢?
多租戶(hù):就是多個(gè)公司/客戶(hù)公用一套系統(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)證:判斷用戶(hù)名密碼是否正確。
鑒權(quán):判斷用戶(hù)是否有權(quán)限訪(fǎng)問(wèn)該接口。
這里本文著重講解,如果遇到shiro相關(guān)應(yīng)用,可以項(xiàng)目直接移植使用。

1.CustomShiroFilterFactoryBean
該類(lèi)主要負(fù)責(zé)解決資源中文路徑問(wèn)題。這里有個(gè)通用的解決方式。
新建類(lèi)集成ShiroFilterFactoryBean方法,并重寫(xiě)核心方法createInstance(),并在注入時(shí),注入新建的類(lèi)CustomShiroFilterFactoryBean,這樣就達(dá)到的以往重新源碼的功能。因?yàn)閟pring提供的功能都是用該思想,所以修改源碼的地方就原來(lái)越少了,都可以使用該方式實(shí)現(xiàn)。
2.JwtFilter
同上文,復(fù)寫(xiě)BasicHttpAuthenticationFilter的驗(yàn)證登錄用戶(hù)的方法,在執(zhí)行登錄接口后判斷用戶(hù)是否正確。
3.ResourceCheckFilter
負(fù)責(zé)鑒權(quán)使用,判斷當(dāng)前用戶(hù)是否有權(quán)限訪(fǎng)問(wèn)。
//表示是否允許訪(fǎng)問(wèn) ,如果允許訪(fǎng)問(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)前用戶(hù)正在訪(fǎng)問(wèn)的 url => " + url);
return subject.isPermitted(url);
}
//onAccessDenied:表示當(dāng)訪(fǎ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)啥的,表示不在走以下的攔截器了(如果還有配置的話(huà))
return false;
}
4.ShiroRealm
主要負(fù)責(zé)獲取用戶(hù)所有的菜單權(quán)限,并提供token的一系列方法。
//獲取所有菜單權(quán)限集合
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
}
//驗(yàn)證用戶(hù)輸入的賬號(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)前用戶(hù)的權(quán)限認(rèn)證緩存
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
5.ShiroConfig
此為shiro的核心配置類(lèi),大多數(shù)寫(xiě)法都是固定寫(xiě)法。
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訪(fǎng)問(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自定義配置類(lèi)
@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();
}
//開(kāi)啟shiro注解
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
// 使用redis緩存用戶(hù)信息 ,并設(shè)置redis
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
//配置redis實(shí)例
redisCacheManager.setRedisManager(redisManager());
//redis中針對(duì)不同用戶(hù)緩存(此處的id需要對(duì)應(yīng)user實(shí)體中的id字段,用于唯一標(biāo)識(shí))
redisCacheManager.setPrincipalIdFieldName("id");
//用戶(hù)權(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配置獲取是否開(kāi)啟第三方接入驗(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文檔配置類(lèi),如果有需要請(qǐng)執(zhí)行復(fù)制使用。

11.WebMvcConfiguration
springboot的常用配置,如跨域配置,精度丟失配置,靜態(tài)資源配置。都是固定寫(xiě)法,如果需要請(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)類(lèi)與使用方法,不多說(shuō) ,比較簡(jiǎn)單。

1.TransmitUserTokenFilter、UserTokenContext
主要負(fù)責(zé)將token放在上下文中。

2.JeecgRedisListerer、RedisReceiver
這里是發(fā)送消息模板的封裝。核心是從上下文中的getbean方法動(dòng)態(tài)的指定想要調(diào)用的JeecgRedisListerer實(shí)現(xiàn)類(lèi)。一種多態(tài)的實(shí)現(xiàn)方式。

3.JeecgRedisCacheWriter
可以看到思想還是上文所說(shuō),將RedisCacheWriter類(lèi)中的方法全部復(fù)制出來(lái),并生成新類(lèi)JeecgRedisCacheWriter,在新類(lèi)中修改,他的目的是信息模塊在存入緩存時(shí),有統(tǒng)一的前綴。在使用時(shí),注入使用JeecgRedisCacheWriter即可,跟修改源碼有這一樣效果,但是更加優(yōu)雅??梢钥闯鰏pring的設(shè)計(jì)思想是多牛批。

3.測(cè)試包jeecg-boot-base
主要負(fù)責(zé)調(diào)用其他功能,沒(méi)啥實(shí)質(zhì)意義。
下圖類(lèi)是xxljob執(zhí)行定時(shí)任務(wù)時(shí)的寫(xiě)法,可以看一看。

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開(kāi)源項(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)聽(tīng)消息隊(duì)列時(shí),使用以下方法即可。原因是必須加入ack與nack。防止隊(duì)列占用。
使用時(shí),該類(lèi)繼承了BaseRabbiMqHandler,并使用父類(lèi)的方法,并使用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) {
//使用了父類(lèi)的方法
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注解的類(lèi)
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);
//類(lèi)上有注解 就創(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ā)送消息使用,多類(lèi)別的消息會(huì)實(shí)現(xiàn)JeecgBusEventHandler類(lèi),而BaseApplicationEvent通過(guò)消息類(lèi)型傳入的不同的參數(shù)選擇合適的業(yè)務(wù)類(lèi)發(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ù)開(kāi)始,休眠三秒");
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開(kāi)始,休眠十秒");
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
該類(lèi)主要是jLock的切面類(lèi),通過(guò)jLock注解參數(shù),判斷需要加鎖的類(lèi)型,同時(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ù)判斷加鎖類(lèi)型
@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();
//判斷鎖類(lèi)型
if (lockModel.equals(LockModel.AUTO)) {
if (keys.length > 1) {
lockModel = LockModel.REDLOCK;
} else {
lockModel = LockModel.REENTRANT;
}
}
//根據(jù)不同的鎖類(lèi)型執(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客戶(hù)端,提供了一大波方法,請(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連接類(lèi)型,通過(guò)根據(jù)該參數(shù)動(dòng)態(tài)的選擇策略類(lè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ù)連接類(lèi)型創(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),此類(lèi)是指定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)槠渌亩际情_(kāi)源項(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í),將用戶(hù)信息放在請(qǐng)求頭中。

5.SentinelFilterContextConfig
使Sentinel鏈路流控模式生效,固定寫(xiě)法。
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ī)范,代碼的寫(xiě)法,代碼的格式化等方面,并不是特別統(tǒng)一,且沒(méi)有嚴(yán)格規(guī)范。總體來(lái)說(shuō)非常適合做私活與畢業(yè)設(shè)計(jì),同時(shí)也是最早一批開(kāi)源的前后端項(xiàng)目腳手架,爆贊。
到此這篇關(guān)于java開(kāi)源項(xiàng)目jeecgboot的文章就介紹到這了,更多相關(guān)java開(kāi)源項(xiàng)目jeecgboot內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot版本升級(jí)容易遇到的一些問(wèn)題
由于項(xiàng)目需求,需要將nacos 1.4.6版本升級(jí)到2.x版本,由此引發(fā)的springboot、springcloud、springcloud Alibaba一系列版本變更,本文給大家總結(jié)一下SpringBoot版本升級(jí)容易遇到的一些問(wèn)題,需要的朋友可以參考下2023-12-12
ShardingSphere-Proxy5搭建使用過(guò)程分析
ShardingSphere-Proxy是跨語(yǔ)言的數(shù)據(jù)庫(kù)代理服務(wù)端,主要用來(lái)處理:分表、分庫(kù)、讀寫(xiě)分離 等,這篇文章主要介紹了ShardingSphere-Proxy5搭建使用過(guò)程,需要的朋友可以參考下2022-10-10
Java中Date類(lèi)和Calendar類(lèi)的常用實(shí)例小結(jié)
這篇文章主要介紹了Java中Date類(lèi)和Calendar類(lèi)的常用實(shí)例小結(jié),是Java入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí)的運(yùn)用,需要的朋友可以參考下2015-08-08
spring boot實(shí)現(xiàn)圖片上傳和下載功能
這篇文章主要為大家詳細(xì)介紹了spring boot實(shí)現(xiàn)圖片上傳和下載功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02
java調(diào)用webservice接口,并解析返回參數(shù)問(wèn)題
這篇文章主要介紹了java調(diào)用webservice接口,并解析返回參數(shù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
Spring boot集成spring session實(shí)現(xiàn)session共享的方法
這篇文章主要介紹了Spring boot集成spring session實(shí)現(xiàn)session共享的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-06-06
SpringBoot基于過(guò)濾器和內(nèi)存實(shí)現(xiàn)重復(fù)請(qǐng)求攔截功能
這篇文章主要介紹了SpringBoot基于過(guò)濾器和內(nèi)存實(shí)現(xiàn)重復(fù)請(qǐng)求攔截,這里我們使用過(guò)濾器的方式對(duì)進(jìn)入服務(wù)器的請(qǐng)求進(jìn)行過(guò)濾操作,實(shí)現(xiàn)對(duì)相同客戶(hù)端請(qǐng)求同一個(gè)接口的過(guò)濾,需要的朋友可以參考下2023-01-01

